Skip to content

Commit 6683da0

Browse files
jakebaileyDonJayamanne
authored andcommitted
Fix colon-triggered block formatting (#2724)
* Dispatch on-type formatters to prevent them from overriding each other * Move providers out of dispatcher, write tests * Add news entry
1 parent 4ecef87 commit 6683da0

File tree

4 files changed

+131
-2
lines changed

4 files changed

+131
-2
lines changed

news/2 Fixes/2714.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix colon-triggered block formatting

src/client/extension.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { EDITOR_LOAD } from './telemetry/constants';
5959
import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry';
6060
import { ICodeExecutionManager, ITerminalAutoActivation } from './terminals/types';
6161
import { BlockFormatProviders } from './typeFormatters/blockFormatProvider';
62+
import { OnTypeFormattingDispatcher } from './typeFormatters/dispatcher';
6263
import { OnEnterFormatter } from './typeFormatters/onEnterFormatter';
6364
import { TEST_OUTPUT_CHANNEL } from './unittests/common/constants';
6465
import { registerTypes as unitTestsRegisterTypes } from './unittests/serviceRegistry';
@@ -136,8 +137,14 @@ export async function activate(context: ExtensionContext): Promise<IExtensionApi
136137
context.subscriptions.push(languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider));
137138
}
138139

139-
context.subscriptions.push(languages.registerOnTypeFormattingEditProvider(PYTHON, new BlockFormatProviders(), ':'));
140-
context.subscriptions.push(languages.registerOnTypeFormattingEditProvider(PYTHON, new OnEnterFormatter(), '\n'));
140+
const onTypeDispatcher = new OnTypeFormattingDispatcher({
141+
'\n': new OnEnterFormatter(),
142+
':': new BlockFormatProviders()
143+
});
144+
const onTypeTriggers = onTypeDispatcher.getTriggerCharacters();
145+
if (onTypeTriggers) {
146+
context.subscriptions.push(languages.registerOnTypeFormattingEditProvider(PYTHON, onTypeDispatcher, onTypeTriggers.first, ...onTypeTriggers.more));
147+
}
141148

142149
const deprecationMgr = serviceContainer.get<IFeatureDeprecationManager>(IFeatureDeprecationManager);
143150
deprecationMgr.initialize();
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { CancellationToken, FormattingOptions, OnTypeFormattingEditProvider, Position, ProviderResult, TextDocument, TextEdit } from 'vscode';
7+
8+
export class OnTypeFormattingDispatcher implements OnTypeFormattingEditProvider {
9+
private readonly providers: { [key: string]: OnTypeFormattingEditProvider };
10+
11+
constructor(providers: { [key: string]: OnTypeFormattingEditProvider }) {
12+
this.providers = providers;
13+
}
14+
15+
public provideOnTypeFormattingEdits(document: TextDocument, position: Position, ch: string, options: FormattingOptions, cancellationToken: CancellationToken): ProviderResult<TextEdit[]> {
16+
const provider = this.providers[ch];
17+
18+
if (provider) {
19+
return provider.provideOnTypeFormattingEdits(document, position, ch, options, cancellationToken);
20+
}
21+
22+
return [];
23+
}
24+
25+
public getTriggerCharacters(): { first: string; more: string[] } | undefined {
26+
let keys = Object.keys(this.providers);
27+
keys = keys.sort(); // Make output deterministic
28+
29+
const first = keys.shift();
30+
31+
if (first) {
32+
return {
33+
first: first,
34+
more: keys
35+
};
36+
}
37+
38+
return undefined;
39+
}
40+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import * as assert from 'assert';
7+
import * as TypeMoq from 'typemoq';
8+
import { CancellationToken, FormattingOptions, OnTypeFormattingEditProvider, Position, ProviderResult, TextDocument, TextEdit } from 'vscode';
9+
import { OnTypeFormattingDispatcher } from '../../client/typeFormatters/dispatcher';
10+
11+
suite('Formatting - Dispatcher', () => {
12+
const doc = TypeMoq.Mock.ofType<TextDocument>();
13+
const pos = TypeMoq.Mock.ofType<Position>();
14+
const opt = TypeMoq.Mock.ofType<FormattingOptions>();
15+
const token = TypeMoq.Mock.ofType<CancellationToken>();
16+
const edits = TypeMoq.Mock.ofType<ProviderResult<TextEdit[]>>();
17+
18+
test('No providers', async () => {
19+
const dispatcher = new OnTypeFormattingDispatcher({});
20+
21+
const triggers = dispatcher.getTriggerCharacters();
22+
assert.equal(triggers, undefined, 'Trigger was not undefined');
23+
24+
const result = await dispatcher.provideOnTypeFormattingEdits(doc.object, pos.object, '\n', opt.object, token.object);
25+
assert.deepStrictEqual(result, [], 'Did not return an empty list of edits');
26+
});
27+
28+
test('Single provider', () => {
29+
const provider = setupProvider(doc.object, pos.object, ':', opt.object, token.object, edits.object);
30+
31+
const dispatcher = new OnTypeFormattingDispatcher({
32+
':': provider.object
33+
});
34+
35+
const triggers = dispatcher.getTriggerCharacters();
36+
assert.deepStrictEqual(triggers, { first: ':', more: [] }, 'Did not return correct triggers');
37+
38+
const result = dispatcher.provideOnTypeFormattingEdits(doc.object, pos.object, ':', opt.object, token.object);
39+
assert.equal(result, edits.object, 'Did not return correct edits');
40+
41+
provider.verifyAll();
42+
});
43+
44+
test('Two providers', () => {
45+
const colonProvider = setupProvider(doc.object, pos.object, ':', opt.object, token.object, edits.object);
46+
47+
const doc2 = TypeMoq.Mock.ofType<TextDocument>();
48+
const pos2 = TypeMoq.Mock.ofType<Position>();
49+
const opt2 = TypeMoq.Mock.ofType<FormattingOptions>();
50+
const token2 = TypeMoq.Mock.ofType<CancellationToken>();
51+
const edits2 = TypeMoq.Mock.ofType<ProviderResult<TextEdit[]>>();
52+
53+
const newlineProvider = setupProvider(doc2.object, pos2.object, '\n', opt2.object, token2.object, edits2.object);
54+
55+
const dispatcher = new OnTypeFormattingDispatcher({
56+
':': colonProvider.object,
57+
'\n': newlineProvider.object
58+
});
59+
60+
const triggers = dispatcher.getTriggerCharacters();
61+
assert.deepStrictEqual(triggers, { first: '\n', more: [':'] }, 'Did not return correct triggers');
62+
63+
const result = dispatcher.provideOnTypeFormattingEdits(doc.object, pos.object, ':', opt.object, token.object);
64+
assert.equal(result, edits.object, 'Did not return correct editsfor colon provider');
65+
66+
const result2 = dispatcher.provideOnTypeFormattingEdits(doc2.object, pos2.object, '\n', opt2.object, token2.object);
67+
assert.equal(result2, edits2.object, 'Did not return correct edits for newline provider');
68+
69+
colonProvider.verifyAll();
70+
newlineProvider.verifyAll();
71+
});
72+
73+
function setupProvider(document: TextDocument, position: Position, ch: string, options: FormattingOptions, cancellationToken: CancellationToken,
74+
result: ProviderResult<TextEdit[]>): TypeMoq.IMock<OnTypeFormattingEditProvider> {
75+
const provider = TypeMoq.Mock.ofType<OnTypeFormattingEditProvider>();
76+
provider.setup(p => p.provideOnTypeFormattingEdits(document, position, ch, options, cancellationToken))
77+
.returns(() => result)
78+
.verifiable(TypeMoq.Times.once());
79+
return provider;
80+
}
81+
});

0 commit comments

Comments
 (0)