Skip to content

Commit 2454423

Browse files
Add regex to dedent else and friends (#6497)
* Dedent on elif|else * Add unit tests (will conflict with #4241) * More descriptive comment (same as f5daf78) * Move tests from extension to language config file * Add news file * Use regex we just introduced * Space is free * Update regexes and existing tests (so they pass) * Add support for open tuples and dictionaries * Shuffle and add tests * Typo + more descriptive key name * Updated news file after new changes * Apply suggestions from code review (first batch) Co-Authored-By: Eric Snow <[email protected]> * Update src/client/language/languageConfiguration.ts Co-Authored-By: Eric Snow <[email protected]> * Third batch of review fixes * Add some comments * Separate unit tests * More test simplification * Simplify return regex * Remove unnecessary tslint disable rule * pipeline is stuck
1 parent 26dc686 commit 2454423

File tree

4 files changed

+94
-20
lines changed

4 files changed

+94
-20
lines changed

news/2 Fixes/6333.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Clarify regexes used for decreasing indentation.

src/client/extension.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { registerTypes as appRegisterTypes } from './application/serviceRegistry
4040
import { IApplicationDiagnostics } from './application/types';
4141
import { DebugService } from './common/application/debugService';
4242
import { IApplicationShell, ICommandManager, IWorkspaceService } from './common/application/types';
43-
import { Commands, isTestExecution, PYTHON, STANDARD_OUTPUT_CHANNEL } from './common/constants';
43+
import { Commands, isTestExecution, PYTHON, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from './common/constants';
4444
import { registerTypes as registerDotNetTypes } from './common/dotnet/serviceRegistry';
4545
import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry';
4646
import { traceError } from './common/logger';
@@ -85,7 +85,7 @@ import { registerTypes as interpretersRegisterTypes } from './interpreter/servic
8585
import { ServiceContainer } from './ioc/container';
8686
import { ServiceManager } from './ioc/serviceManager';
8787
import { IServiceContainer, IServiceManager } from './ioc/types';
88-
import { setLanguageConfiguration } from './language/languageConfiguration';
88+
import { getLanguageConfiguration } from './language/languageConfiguration';
8989
import { LinterCommands } from './linters/linterCommands';
9090
import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry';
9191
import { ILintingEngine } from './linters/types';
@@ -173,7 +173,7 @@ async function activateUnsafe(context: ExtensionContext): Promise<IExtensionApi>
173173
const linterProvider = new LinterProvider(context, serviceManager);
174174
context.subscriptions.push(linterProvider);
175175

176-
setLanguageConfiguration();
176+
languages.setLanguageConfiguration(PYTHON_LANGUAGE, getLanguageConfiguration());
177177

178178
if (pythonSettings && pythonSettings.formatting && pythonSettings.formatting.provider !== 'internalConsole') {
179179
const formatProvider = new PythonFormattingEditProvider(context, serviceContainer);

src/client/language/languageConfiguration.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,24 @@
22
// Licensed under the MIT License.
33
'use strict';
44

5-
import { IndentAction, languages } from 'vscode';
6-
import { PYTHON_LANGUAGE } from '../common/constants';
5+
import { IndentAction } from 'vscode';
76

87
export const MULTILINE_SEPARATOR_INDENT_REGEX = /^(?!\s+\\)[^#\n]+\\$/;
8+
/**
9+
* This does not handle all cases. However, it does handle nearly all usage.
10+
* Here's what it does not cover:
11+
* - the statement is split over multiple lines (and hence the ":" is on a different line)
12+
* - the code block is inlined (after the ":")
13+
* - there are multiple statements on the line (separated by semicolons)
14+
* Also note that `lambda` is purposefully excluded.
15+
*/
16+
export const INCREASE_INDENT_REGEX = /^\s*(?:(?:async|class|def|elif|except|for|if|while|with)\b.*|(else|finally|try))\s*:\s*(#.*)?$/;
17+
export const DECREASE_INDENT_REGEX = /^\s*(?:else|finally|(?:elif|except)\b.*)\s*:\s*(#.*)?$/;
18+
export const OUTDENT_ONENTER_REGEX = /^\s*(?:break|continue|pass|(?:raise|return)\b.*)\s*(#.*)?$/;
919

10-
export function setLanguageConfiguration() {
11-
// Enable indentAction
12-
languages.setLanguageConfiguration(PYTHON_LANGUAGE, {
20+
export function getLanguageConfiguration() {
21+
return {
1322
onEnterRules: [
14-
{
15-
beforeText: /^\s*(?:def|class|for|if|elif|else|while|try|with|finally|except|async)\b.*:\s*/,
16-
action: { indentAction: IndentAction.Indent }
17-
},
1823
{
1924
beforeText: MULTILINE_SEPARATOR_INDENT_REGEX,
2025
action: { indentAction: IndentAction.Indent }
@@ -25,10 +30,13 @@ export function setLanguageConfiguration() {
2530
action: { indentAction: IndentAction.None, appendText: '# ' }
2631
},
2732
{
28-
beforeText: /^\s+(continue|break|return)\b.*/,
29-
afterText: /\s+$/,
33+
beforeText: OUTDENT_ONENTER_REGEX,
3034
action: { indentAction: IndentAction.Outdent }
3135
}
32-
]
33-
});
36+
],
37+
indentationRules: {
38+
increaseIndentPattern: INCREASE_INDENT_REGEX,
39+
decreaseIndentPattern: DECREASE_INDENT_REGEX
40+
}
41+
};
3442
}

src/test/language/languageConfiguration.unit.test.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,84 @@
55

66
import { expect } from 'chai';
77

8-
import { MULTILINE_SEPARATOR_INDENT_REGEX } from '../../client/language/languageConfiguration';
8+
import { DECREASE_INDENT_REGEX, INCREASE_INDENT_REGEX, MULTILINE_SEPARATOR_INDENT_REGEX, OUTDENT_ONENTER_REGEX } from '../../client/language/languageConfiguration';
99

1010
suite('Language configuration regexes', () => {
1111
test('Multiline separator indent regex should not pick up strings with no multiline separator', async () => {
1212
const result = MULTILINE_SEPARATOR_INDENT_REGEX.test('a = "test"');
13-
expect (result).to.be.equal(false, 'Multiline separator indent regex for regular strings should not have matches');
13+
expect(result).to.be.equal(false, 'Multiline separator indent regex for regular strings should not have matches');
1414
});
15+
1516
test('Multiline separator indent regex should not pick up strings with escaped characters', async () => {
1617
const result = MULTILINE_SEPARATOR_INDENT_REGEX.test('a = \'hello \\n\'');
17-
expect (result).to.be.equal(false, 'Multiline separator indent regex for strings with escaped characters should not have matches');
18+
expect(result).to.be.equal(false, 'Multiline separator indent regex for strings with escaped characters should not have matches');
1819
});
20+
1921
test('Multiline separator indent regex should pick up strings ending with a multiline separator', async () => {
2022
const result = MULTILINE_SEPARATOR_INDENT_REGEX.test('a = \'multiline \\');
21-
expect (result).to.be.equal(true, 'Multiline separator indent regex for strings with newline separator should have matches');
23+
expect(result).to.be.equal(true, 'Multiline separator indent regex for strings with newline separator should have matches');
24+
});
25+
26+
[
27+
'async def test(self):',
28+
'class TestClass:',
29+
'def foo(self, node, namespace=""):',
30+
'for item in items:',
31+
'if foo is None:',
32+
'try:',
33+
'while \'::\' in macaddress:',
34+
'with self.test:'
35+
].forEach(example => {
36+
const keyword = example.split(' ')[0];
37+
38+
test(`Increase indent regex should pick up lines containing the ${keyword} keyword`, async () => {
39+
const result = INCREASE_INDENT_REGEX.test(example);
40+
expect(result).to.be.equal(true, `Increase indent regex should pick up lines containing the ${keyword} keyword`);
41+
});
42+
43+
test(`Decrease indent regex should not pick up lines containing the ${keyword} keyword`, async () => {
44+
const result = DECREASE_INDENT_REGEX.test(example);
45+
expect(result).to.be.equal(false, `Decrease indent regex should not pick up lines containing the ${keyword} keyword`);
46+
});
47+
});
48+
49+
['elif x < 5:', 'else:', 'except TestError:', 'finally:'].forEach(example => {
50+
const keyword = example.split(' ')[0];
51+
52+
test(`Increase indent regex should pick up lines containing the ${keyword} keyword`, async () => {
53+
const result = INCREASE_INDENT_REGEX.test(example);
54+
expect(result).to.be.equal(true, `Increase indent regex should pick up lines containing the ${keyword} keyword`);
55+
});
56+
57+
test(`Decrease indent regex should pick up lines containing the ${keyword} keyword`, async () => {
58+
const result = DECREASE_INDENT_REGEX.test(example);
59+
expect(result).to.be.equal(true, `Decrease indent regex should pick up lines containing the ${keyword} keyword`);
60+
});
61+
});
62+
63+
test('Increase indent regex should not pick up lines without keywords', async () => {
64+
const result = INCREASE_INDENT_REGEX.test('a = \'hello \\n \'');
65+
expect(result).to.be.equal(false, 'Increase indent regex should not pick up lines without keywords');
66+
});
67+
68+
test('Decrease indent regex should not pick up lines without keywords', async () => {
69+
const result = DECREASE_INDENT_REGEX.test('a = \'hello \\n \'');
70+
expect(result).to.be.equal(false, 'Decrease indent regex should not pick up lines without keywords');
71+
});
72+
73+
[' break', '\t\t continue', ' pass', 'raise Exception(\'Unknown Exception\'', ' return [ True, False, False ]'].forEach(example => {
74+
const keyword = example.trim().split(' ')[0];
75+
76+
const testWithoutComments = `Outdent regex for on enter rule should pick up lines containing the ${keyword} keyword`;
77+
test(testWithoutComments, () => {
78+
const result = OUTDENT_ONENTER_REGEX.test(example);
79+
expect(result).to.be.equal(true, testWithoutComments);
80+
});
81+
82+
const testWithComments = `Outdent regex on enter should pick up lines containing the ${keyword} keyword and ending with comments`;
83+
test(testWithComments, () => {
84+
const result = OUTDENT_ONENTER_REGEX.test(`${example} # test comment`);
85+
expect(result).to.be.equal(true, testWithComments);
86+
});
2287
});
2388
});

0 commit comments

Comments
 (0)