Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.3.14] - 2026-09-23

### Fixed
- Improved code suggestions when MATLAB is stopped in the debugger (Addresses [mathworks/MATLAB-extension-for-vscode#138](https://github.com/mathworks/MATLAB-extension-for-vscode/issues/251))

## [1.3.13] - 2026-07-28

### Added
Expand Down
5 changes: 3 additions & 2 deletions matlab/+matlabls/+handlers/+testing/TestStreamingPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ function publishEvent(plugin, event)
ts = pluginData.TestSuite;
tc = ts.TestClass;
if strlength(tc) > 0
testFile = fullfile(char(ts.BaseFolder), [char(tc) '.m']);
qualifiedName = char(tc);
else
parts = strsplit(char(pluginData.Name), '/');
testFile = fullfile(char(ts.BaseFolder), [parts{1} '.m']);
qualifiedName = parts{1};
end
testFile = matlabls.handlers.testing.qualifiedNameToPath(char(ts.BaseFolder), qualifiedName);
end


Expand Down
25 changes: 23 additions & 2 deletions matlab/+matlabls/+handlers/+testing/discoverTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
end
end
suite = [suites{:}];

% Before R2022a, TestSuite.fromFolder does not descend into namespace
% ("+") folders, so supplement the suite with any namespaced tests.
if strcmp(mode, 'folder') && isMATLABReleaseOlderThan('R2022a')
suite = [suite, discoverNamespacedTests(paths)];
end

warnMsg = lastwarn;
warnMsg = regexprep(warnMsg, '<a[^>]*>', '');
warnMsg = strrep(warnMsg, '</a>', '');
Expand All @@ -45,11 +52,11 @@
tc = suite(i).TestClass;
if strlength(tc) > 0
testParentNames{i} = char(tc);
filenames{i} = fullfile(char(suite(i).BaseFolder), [char(tc) '.m']);
filenames{i} = matlabls.handlers.testing.qualifiedNameToPath(char(suite(i).BaseFolder), char(tc));
else
parts = strsplit(fullName, '/');
testParentNames{i} = parts{1};
filenames{i} = fullfile(char(suite(i).BaseFolder), [parts{1} '.m']);
filenames{i} = matlabls.handlers.testing.qualifiedNameToPath(char(suite(i).BaseFolder), parts{1});
end

parenIdx = strfind(fullName, '(');
Expand Down Expand Up @@ -77,3 +84,17 @@
result.warning = '';
end
end

function suite = discoverNamespacedTests(paths)
% Build a suite from every test file within namespace ("+") folders under
% the given paths, adding each file directly since fromFolder skips them
% on releases before R2022a.
suite = matlab.unittest.Test.empty(1, 0);
for i = 1:numel(paths)
nsFiles = dir(fullfile(paths{i}, '**', '+*', '*.m'));
for j = 1:numel(nsFiles)
file = fullfile(nsFiles(j).folder, nsFiles(j).name);
suite = [suite, matlab.unittest.TestSuite.fromFile(file)]; %#ok<AGROW>
end
end
end
15 changes: 15 additions & 0 deletions matlab/+matlabls/+handlers/+testing/qualifiedNameToPath.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
function p = qualifiedNameToPath(baseFolder, qualifiedName)

% Copyright 2026 The MathWorks, Inc.

parts = strsplit(qualifiedName, '.');
folders = strcat('+', parts(1:end-1));
p = fullfile(baseFolder, folders{:}, [parts{end} '.m']);

if ~isfile(p)
resolved = which(qualifiedName);
if ~isempty(resolved) && isfile(resolved)
p = resolved;
end
end
end
Binary file modified matlab/+matlabls/+internal/computeCodeData.p
Binary file not shown.
Binary file added matlab/+matlabls/+internal/getCompletions.p
Binary file not shown.
Binary file removed matlab/+matlabls/+internal/getCompletionsData.p
Binary file not shown.
Binary file added matlab/+matlabls/+internal/parseConditionalBlocks.p
Binary file not shown.
86 changes: 86 additions & 0 deletions matlab/+matlabls/+variableviewer/VariableViewerService.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
classdef VariableViewerService < handle
% Pub/sub service for Variable Viewer communication with the VS Code extension.

% Copyright 2026 The MathWorks, Inc.

properties (Constant, Access = private)
ClientChannel = '/VariableViewer/ClientMsg'
ServerChannel = '/VariableViewer/ServerMsg'
end

methods (Static)
function setupListeners ()
matlabls.internal.CommunicationManager.subscribe( ...
matlabls.variableviewer.VariableViewerService.ClientChannel, ...
@matlabls.variableviewer.VariableViewerService.handleClientMessage);
end
end

methods (Static, Hidden)
function handleClientMessage (msg)
try
switch msg.type
case 'QueryVariable'
response = matlabls.variableviewer.getVariablePage(msg.varName, 1, 1);
case 'QueryPage'
response = matlabls.variableviewer.getVariablePage(msg.varName, msg.startRow, msg.startCol);
end
response = matlabls.variableviewer.VariableViewerService.buildResponse(response, msg.varName);
matlabls.internal.CommunicationManager.publish( ...
matlabls.variableviewer.VariableViewerService.ServerChannel, response);
catch ME
errorResponse.type = "Error";
errorResponse.varName = string(msg.varName);
errorResponse.requestType = string(msg.type);
errorResponse.message = string(ME.message);
matlabls.internal.CommunicationManager.publish( ...
matlabls.variableviewer.VariableViewerService.ServerChannel, errorResponse);
end
end
end

methods (Static, Access = private)
function response = buildResponse (response, varName)
response.type = "VariableResponse";
response.varName = string(varName);

% Normalize metadata.size to always serialize as an array
response.metadata.size = num2cell(response.metadata.size);

% Normalize labels to always serialize as arrays
if isfield(response, 'columns') && isfield(response.columns, 'labels')
response.columns.labels = matlabls.variableviewer.VariableViewerService.ensureCellArray(response.columns.labels);
end
if isfield(response, 'rows') && isfield(response.rows, 'labels')
response.rows.labels = matlabls.variableviewer.VariableViewerService.ensureCellArray(response.rows.labels);
end

% Normalize cells.data to always serialize as nested array
if isfield(response, 'cells') && isfield(response.cells, 'data')
response.cells.data = matlabls.variableviewer.VariableViewerService.ensureNestedCellArray(response.cells.data);
end
end

function out = ensureCellArray (structArray)
% Wrap struct array in a cell array so single-element arrays serialize as [{}] not {}
if isstruct(structArray)
out = num2cell(structArray);
else
out = structArray;
end
end

function out = ensureNestedCellArray (data)
% Ensure string matrix serializes as [["a","b"],["c","d"]] not ["a","b"]
if isstring(data)
nRows = size(data, 1);
out = cell(nRows, 1);
for r = 1:nRows
out{r} = cellstr(data(r, :));
end
else
out = data;
end
end
end
end
189 changes: 189 additions & 0 deletions matlab/+matlabls/+variableviewer/getVariablePage.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
function response = getVariablePage(varName, startRow, startCol, workspace)
%GETVARIABLEPAGE Return a page of data and metadata for a variable.
%
% response = getVariablePage("A", 1, 1)
% response = getVariablePage("T", 1, 1, "base")
%
% startRow and startCol are 1-based indices specifying the top-left
% corner of the page window into the variable's data.

% Copyright 2026 The MathWorks, Inc.

if nargin < 4
workspace = "base";
end

if ~isMATLABReleaseOlderThan("R2027b")
response = getPage(varName, startRow, startCol, workspace);
else
response = getPageLegacy(varName, startRow, startCol, workspace);
end
end

%% Constants

function [rows, cols] = pageSize()
rows = 50;
cols = 20;
end

%% Support checks

function tf = isResponseSupported(response)
metadata = response.metadata;
supportedClasses = ["double","single","int8","int16","int32","int64","uint8","uint16","uint32","uint64","logical","table"];

% Only 2D data is currently supported — check for sparse, tall, and N-D matrices
tf = ismember(metadata.dataType, supportedClasses) ...
&& ~metadata.isSparse ...
&& ~metadata.isTall ...
&& numel(metadata.size) <= 2;
end

%% R2027a+ path — delegates to the internal pipeline
function response = getPage(varName, startRow, startCol, workspace)
[pageRows, pageCols] = pageSize();
response = matlab.datatools.uiservices.variabledisplay.internal.pipeline.getVariableView(...
varName, ...
Workspace=workspace, ...
StartRow=startRow, ...
StartCol=startCol, ...
FlattenColumns=true, ...
PageSize=[pageRows, pageCols], ...
MetadataFcn=@addSparseTallFlags);

if ~isResponseSupported(response)
response.preview = unsupportedPreview(evalin(workspace, varName));
end
end

function metadata = addSparseTallFlags(metadata, varValue)
metadata.isSparse = issparse(varValue);
metadata.isTall = istall(varValue);
end

%% Legacy path — evalin once, build metadata, return cells or preview
function response = getPageLegacy(varName, startRow, startCol, workspace)
[pageRows, pageCols] = pageSize();

varValue = evalin(workspace, varName);

response.metadata.size = size(varValue);
response.metadata.dataType = string(class(varValue));
response.metadata = addSparseTallFlags(response.metadata, varValue);

if ~isResponseSupported(response)
response.preview = unsupportedPreview(varValue);
return
end

if istable(varValue)
response = buildTablePage(response, varValue, startRow, startCol, pageRows, pageCols);
else
response = buildNumericPage(response, varValue, startRow, startCol, pageRows, pageCols);
end
end

function response = buildNumericPage(response, varValue, startRow, startCol, pageRows, pageCols)
[nRows, nCols] = size(varValue);

if nRows == 0 || nCols == 0
return
end

startRow = max(1, startRow);
startCol = max(1, startCol);

if startRow > nRows || startCol > nCols
error('getVariablePage:OutOfBounds', 'Requested page starts outside array bounds.');
end

endRow = min(startRow + pageRows - 1, nRows);
endCol = min(startCol + pageCols - 1, nCols);

page = varValue(startRow:endRow, startCol:endCol);
if islogical(page)
page = double(page);
end
response.cells.data = string(page);
response.cells.startRow = startRow;
response.cells.startCol = startCol;
end

function response = buildTablePage(response, varValue, startRow, startCol, pageRows, pageCols)
[nRows, nCols] = size(varValue);
varNames = varValue.Properties.VariableNames;
rowNames = varValue.Properties.RowNames;

if nRows == 0 || nCols == 0
for curCol = 1:min(pageCols, nCols)
response.columns.labels(curCol).id = string(curCol);
response.columns.labels(curCol).name = string(varNames{curCol});
end
return
end

startRow = max(1, startRow);
startCol = max(1, startCol);

if startRow > nRows || startCol > nCols
error('getVariablePage:OutOfBounds', 'Requested page starts outside table bounds.');
end

endRow = min(startRow + pageRows - 1, nRows);
endCol = min(startCol + pageCols - 1, nCols);

% Column labels — scoped to the page window
for curCol = startCol:endCol
localCol = curCol - startCol + 1;
response.columns.labels(localCol).id = string(curCol);
response.columns.labels(localCol).name = string(varNames{curCol});
end

% Row labels — scoped to the page window, only when table has RowNames
if ~isempty(rowNames)
for curRow = startRow:endRow
localRow = curRow - startRow + 1;
response.rows.labels(localRow).id = string(curRow);
response.rows.labels(localRow).name = string(rowNames{curRow});
end
end

% Flatten each cell to a display string
cellData = strings(endRow - startRow + 1, endCol - startCol + 1);
for c = startCol:endCol
for r = startRow:endRow
val = varValue{r, c};
if iscell(val) && isscalar(val)
val = val{1};
end
cellData(r - startRow + 1, c - startCol + 1) = flattenValue(val);
end
end

response.cells.data = cellData;
response.cells.startRow = startRow;
response.cells.startCol = startCol;
end

function s = unsupportedPreview(val)
s = strtrim(formattedDisplayText(val, 'SuppressMarkup', true));
end

function s = flattenValue(val)
if (isstring(val) && isscalar(val)) || ischar(val)
s = string(val);
elseif islogical(val) && isscalar(val)
if val
s = "true";
else
s = "false";
end
elseif isnumeric(val) && isscalar(val)
s = string(num2str(val));
elseif isscalar(val) && (isdatetime(val) || isduration(val) || iscalendarduration(val) || iscategorical(val))
s = string(val);
else
s = strjoin(string(size(val)), "x") + " " + string(class(val));
end
end
7 changes: 5 additions & 2 deletions matlab/initmatlabls.m
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ function initmatlabls (outFile)
% Initializes a MATLAB® session to talk to a MATLAB language server.
% Writes connection info to the outFile specified by the client

% Copyright 2022 - 2025 The MathWorks, Inc.
% Copyright 2022 - 2026 The MathWorks, Inc.

try
disp('matlabls: Beginning initialization')
Expand Down Expand Up @@ -32,7 +32,10 @@ function initmatlabls (outFile)

% Initialize communication manager
matlabls.internal.CommunicationManager.initialize();


% Initialize Variable Viewer pub/sub service
matlabls.variableviewer.VariableViewerService.setupListeners();

if nargin == 1
logConnectionData(outFile)
end
Expand Down
Loading
Loading