Skip to content

Commit c92ed97

Browse files
philipc-mwprabhakk-mw
authored andcommitted
Adds support for auto indentation to the JupyterLab Extension for MATLAB.
1 parent c47a3bf commit c92ed97

File tree

10 files changed

+4683
-1576
lines changed

10 files changed

+4683
-1576
lines changed

src/jupyter_matlab_labextension/.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
"ecmaVersion": 6,
77
"sourceType": "module"
88
},
9+
"env": {
10+
"jest": true
11+
},
912
"plugins": ["@typescript-eslint"],
1013
"extends": ["standard"],
1114
"rules": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright 2025 The MathWorks, Inc.
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'node',
5+
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
6+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
7+
};

src/jupyter_matlab_labextension/package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jupyter_matlab_labextension",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "A JupyterLab extension.",
55
"keywords": [
66
"jupyter",
@@ -45,6 +45,8 @@
4545
"prettier:check": "jlpm prettier:base --check",
4646
"stylelint": "jlpm stylelint:check --fix",
4747
"stylelint:check": "stylelint --cache \"style/**/*.css\"",
48+
"test": "jest && jlpm build:lezer && jlpm test:lezer",
49+
"test:lezer": "cd src/lezer-matlab && npm test",
4850
"watch": "run-p watch:src watch:labextension",
4951
"watch:src": "tsc -w",
5052
"watch:labextension": "jupyter labextension watch .",
@@ -65,6 +67,7 @@
6567
},
6668
"devDependencies": {
6769
"@jupyterlab/builder": ">=4.0.0",
70+
"@types/jest": "^29.5.14",
6871
"@typescript-eslint/eslint-plugin": "^5.62.0",
6972
"@typescript-eslint/parser": "^5.62.0",
7073
"cross-spawn": "^6.0.6",
@@ -75,10 +78,12 @@
7578
"eslint-plugin-n": "^15.0.0",
7679
"eslint-plugin-node": "^11.1.0",
7780
"eslint-plugin-promise": "^6.0.0",
81+
"jest": "^29.7.0",
7882
"npm-run-all": "^4.1.5",
7983
"prettier": "^2.8.7",
8084
"rimraf": "^4.4.1",
8185
"semver": ">=5.7.2",
86+
"ts-jest": "^29.2.5",
8287
"typescript": "~5.0.2",
8388
"ws": "^7.5.10",
8489
"yarn-audit-fix": "^10.1.1"

src/jupyter_matlab_labextension/src/codemirror-lang-matlab/codemirror-lang-matlab.ts

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

33
import { parser } from '../lezer-matlab/dist/index';
4-
import { LRLanguage, LanguageSupport } from '@codemirror/language';
4+
import { indentNodeProp, LanguageSupport, LRLanguage, TreeIndentContext } from '@codemirror/language';
5+
import { lineIndent, getDedentPattern } from './indent-matlab';
6+
7+
function determineLineIndent (context: TreeIndentContext) {
8+
if (context.pos === 0) { return null; }
9+
const currentLine = context.lineAt(context.pos);
10+
const previousLine = currentLine.text.length === 0
11+
? context.lineAt(context.pos, -1) // Look to the left of the simulated line break.
12+
: context.lineAt(context.pos - 1); // Not on a simulated line break, so step back to the previous line.
13+
if (previousLine === null || currentLine === null) {
14+
return null;
15+
}
16+
return lineIndent(context.unit, currentLine.text, previousLine.text);
17+
}
518

619
// Define a CodeMirror language from the Lezer parser.
720
// https://codemirror.net/docs/ref/#language.LRLanguage
821
export const matlabLanguage = LRLanguage.define({
922
name: 'matlab',
10-
parser,
23+
parser: parser.configure({
24+
props: [
25+
indentNodeProp.add({
26+
Script: determineLineIndent
27+
})
28+
]
29+
}),
1130
languageData: {
12-
commentTokens: { line: '%' }
31+
commentTokens: { line: '%' },
32+
indentOnInput: getDedentPattern()
1333
}
1434
});
1535

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2025 The MathWorks, Inc.
2+
3+
// Indent after these keywords unless the line ends with "end".
4+
const indentPattern = /^(?:\s*)(arguments|case|catch|classdef|else|elseif|enumeration|for|function|if|methods|otherwise|parfor|properties|switch|try|while)\b(?!.*\bend;?$)/;
5+
const dedentPattern = /^(?:\s*)(case|catch|else|end|otherwise)\b$/;
6+
const leadingWhitespacePattern = /^\s*/;
7+
8+
export function getDedentPattern (): RegExp {
9+
return new RegExp(dedentPattern);
10+
}
11+
12+
export function lineIndent (indentUnit: number, currentLineText: string, previousLineText: string) {
13+
const prevLeadingWhitespace = previousLineText.match(leadingWhitespacePattern);
14+
const prevLineIndent = prevLeadingWhitespace ? prevLeadingWhitespace[0].length : 0;
15+
if (currentLineText.match(/^(?:\s*)(case)\b$/) && previousLineText.match(/^(?:\s*)(switch)\b/)) {
16+
// First case in a switch statement.
17+
return prevLineIndent + indentUnit;
18+
} else if (currentLineText.match(/^(?:\s*)(end)\b$/)) {
19+
// Treat "end" separately to avoid mistakenly correcting the end of a switch statement.
20+
const currentLeadingWhitespace = currentLineText.match(leadingWhitespacePattern);
21+
const currentLineIndent = currentLeadingWhitespace ? currentLeadingWhitespace[0].length : 0;
22+
const indentMatch = previousLineText.match(indentPattern);
23+
if (indentMatch) {
24+
return Math.min(prevLineIndent, currentLineIndent);
25+
} else if (prevLineIndent >= indentUnit) {
26+
return Math.min(prevLineIndent - indentUnit, currentLineIndent);
27+
} else {
28+
return 0;
29+
}
30+
} else {
31+
// Other cases
32+
let lineIndent = prevLineIndent;
33+
const indentMatch = previousLineText.match(indentPattern);
34+
if (indentMatch !== null) {
35+
lineIndent += indentUnit;
36+
}
37+
const dedentMatch = currentLineText.match(dedentPattern);
38+
if (dedentMatch !== null && lineIndent >= indentUnit) {
39+
lineIndent -= indentUnit;
40+
}
41+
return lineIndent;
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2025 The MathWorks, Inc.
2+
3+
import { lineIndent } from '../indent-matlab';
4+
5+
describe('lineIndent', () => {
6+
const indentUnit = 4;
7+
8+
test('should indent after an indent pattern', () => {
9+
const previousLineText = 'if foo';
10+
const currentLineText = '';
11+
const result = lineIndent(indentUnit, currentLineText, previousLineText);
12+
expect(result).toBe(indentUnit);
13+
});
14+
15+
test('should indent first case in a switch statement by a single indent', () => {
16+
const previousLineText = 'switch foo';
17+
const currentLineText = 'case bar';
18+
const result = lineIndent(indentUnit, currentLineText, previousLineText);
19+
expect(result).toBe(indentUnit);
20+
});
21+
22+
test('should dedent "end" after an indented block', () => {
23+
const previousLineText = 'for c = 1:s';
24+
const currentLineText = 'end';
25+
const result = lineIndent(indentUnit, currentLineText, previousLineText);
26+
expect(result).toBe(0);
27+
});
28+
29+
test('should not change indentation for lines not matching patterns', () => {
30+
const previousLineText = ' foo;';
31+
const currentLineText = '';
32+
const result = lineIndent(indentUnit, currentLineText, previousLineText);
33+
expect(result).toBe(4);
34+
});
35+
36+
test('should not indent after one-line statement', () => {
37+
const previousLineText = 'if foo A=1; else A=2; end;';
38+
const currentLineText = '';
39+
const result = lineIndent(indentUnit, currentLineText, previousLineText);
40+
expect(result).toBe(0);
41+
});
42+
43+
test('should not decrease indentation below zero', () => {
44+
const previousLineText = 'end';
45+
const currentLineText = 'end';
46+
const result = lineIndent(indentUnit, currentLineText, previousLineText);
47+
expect(result).toBe(0);
48+
});
49+
});

src/jupyter_matlab_labextension/src/lezer-matlab/src/matlab.grammar

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

33
@top Script { expression* }
44

@@ -24,7 +24,7 @@ expression {
2424
charVector { '"' (!["\n])* '"' }
2525
stringArray { "'" (!['\n])* "'" }
2626
SystemCommand { "!" (![\n])* }
27-
Symbol { "+" | "-" | "*" | "=" | ";" | ":" | "(" | ")" | "{" | "}" | "[" | "]" }
27+
Symbol { "+" | "-" | "*" | "=" | ";" | ":" | "(" | ")" | "{" | "}" | "[" | "]" | "," }
2828
space { @whitespace+ }
2929
@precedence { SystemCommand, Identifier }
3030
@precedence { SystemCommand, space }

src/jupyter_matlab_labextension/src/lezer-matlab/test/nested_terms.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,4 @@ Script(String,Symbol,String,Symbol)
7878

7979
==>
8080

81-
Script(Magic, Magic, Magic)
81+
Script(Magic, Magic, Magic)

src/jupyter_matlab_labextension/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"strict": true,
2020
"strictNullChecks": true,
2121
"target": "es2018",
22-
"types": []
22+
"types": ["jest"],
23+
"lib": ["DOM", "ES2018", "ES2020.Intl"]
2324
},
2425
"include": ["src"]
2526
}

0 commit comments

Comments
 (0)