Skip to content

Commit b9befa5

Browse files
author
Akos Kitta
committed
Use clang-format as the default sketch formatter.
- Bumped `clangd` to `14.0.0`, - Can use `.clang-format` from: - current sketch folder, - `~/.arduinoIDE/.clang-format`, - `directories#data/.clang-format`, or - falls back to default formatter styles. Closes arduino#1009 Closes arduino#566 Signed-off-by: Akos Kitta <[email protected]>
1 parent 5b486b1 commit b9befa5

File tree

9 files changed

+461
-3
lines changed

9 files changed

+461
-3
lines changed

arduino-ide-extension/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@
163163
"version": "2.0.0"
164164
},
165165
"clangd": {
166-
"version": "13.0.0"
166+
"version": "14.0.0"
167167
},
168168
"languageServer": {
169169
"version": "0.6.0"

arduino-ide-extension/scripts/download-ls.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,24 @@
6666
build,
6767
`arduino-language-server${platform === 'win32' ? '.exe' : ''}`
6868
);
69-
let clangdExecutablePath, lsSuffix, clangdSuffix;
69+
let clangdExecutablePath, clangFormatExecutablePath, lsSuffix, clangdSuffix;
7070

7171
switch (platformArch) {
7272
case 'darwin-x64':
7373
clangdExecutablePath = path.join(build, 'clangd');
74+
clangFormatExecutablePath = path.join(build, 'clang-format');
7475
lsSuffix = 'macOS_64bit.tar.gz';
7576
clangdSuffix = 'macOS_64bit';
7677
break;
7778
case 'linux-x64':
7879
clangdExecutablePath = path.join(build, 'clangd');
80+
clangFormatExecutablePath = path.join(build, 'clang-format');
7981
lsSuffix = 'Linux_64bit.tar.gz';
8082
clangdSuffix = 'Linux_64bit';
8183
break;
8284
case 'win32-x64':
8385
clangdExecutablePath = path.join(build, 'clangd.exe');
86+
clangFormatExecutablePath = path.join(build, 'clang-format.exe');
8487
lsSuffix = 'Windows_64bit.zip';
8588
clangdSuffix = 'Windows_64bit';
8689
break;
@@ -103,4 +106,15 @@
103106
downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, {
104107
strip: 1,
105108
}); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder.
109+
110+
const clangdFormatUrl = `https://downloads.arduino.cc/tools/clang-format_${clangdVersion}_${clangdSuffix}.tar.bz2`;
111+
downloader.downloadUnzipAll(
112+
clangdFormatUrl,
113+
build,
114+
clangFormatExecutablePath,
115+
force,
116+
{
117+
strip: 1,
118+
}
119+
);
106120
})();

arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

+18
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ import {
277277
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
278278
import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager';
279279
import { EditorManager } from './theia/editor/editor-manager';
280+
import { Formatter, FormatterPath } from '../common/protocol/formatter';
281+
import { Format } from './contributions/format';
282+
import { MonacoFormattingConflictsContribution } from './theia/monaco/monaco-formatting-conflicts';
283+
import { MonacoFormattingConflictsContribution as TheiaMonacoFormattingConflictsContribution } from '@theia/monaco/lib/browser/monaco-formatting-conflicts';
280284

281285
const ElementQueries = require('css-element-queries/src/ElementQueries');
282286

@@ -563,6 +567,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
563567
)
564568
.inSingletonScope();
565569

570+
bind(Formatter)
571+
.toDynamicValue(({ container }) =>
572+
WebSocketConnectionProvider.createProxy(container, FormatterPath)
573+
)
574+
.inSingletonScope();
575+
566576
bind(ArduinoFirmwareUploader)
567577
.toDynamicValue((context) =>
568578
WebSocketConnectionProvider.createProxy(
@@ -630,6 +640,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
630640
Contribution.configure(bind, ArchiveSketch);
631641
Contribution.configure(bind, AddZipLibrary);
632642
Contribution.configure(bind, PlotterFrontendContribution);
643+
Contribution.configure(bind, Format);
644+
645+
// Disabled the quick-pick customization from Theia when multiple formatters are available.
646+
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.
647+
bind(MonacoFormattingConflictsContribution).toSelf().inSingletonScope();
648+
rebind(TheiaMonacoFormattingConflictsContribution).toService(
649+
MonacoFormattingConflictsContribution
650+
);
633651

634652
bind(ResponseServiceImpl)
635653
.toSelf()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { MaybePromise } from '@theia/core';
2+
import { inject, injectable } from '@theia/core/shared/inversify';
3+
import * as monaco from '@theia/monaco-editor-core';
4+
import { Formatter } from '../../common/protocol/formatter';
5+
import { Contribution, URI } from './contribution';
6+
7+
@injectable()
8+
export class Format
9+
extends Contribution
10+
implements
11+
monaco.languages.DocumentRangeFormattingEditProvider,
12+
monaco.languages.DocumentFormattingEditProvider
13+
{
14+
@inject(Formatter)
15+
private readonly formatter: Formatter;
16+
17+
override onStart(): MaybePromise<void> {
18+
const selector = this.selectorOf('ino', 'c', 'cpp', 'h', 'hpp', 'pde');
19+
monaco.languages.registerDocumentRangeFormattingEditProvider(
20+
selector,
21+
this
22+
);
23+
monaco.languages.registerDocumentFormattingEditProvider(selector, this);
24+
}
25+
async provideDocumentRangeFormattingEdits(
26+
model: monaco.editor.ITextModel,
27+
range: monaco.Range,
28+
options: monaco.languages.FormattingOptions,
29+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
30+
_token: monaco.CancellationToken
31+
): Promise<monaco.languages.TextEdit[]> {
32+
const text = await this.format(model, range, options);
33+
return [{ range, text }];
34+
}
35+
36+
async provideDocumentFormattingEdits(
37+
model: monaco.editor.ITextModel,
38+
options: monaco.languages.FormattingOptions,
39+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
40+
_token: monaco.CancellationToken
41+
): Promise<monaco.languages.TextEdit[]> {
42+
const range = this.fullRange(model);
43+
const text = await this.format(model, range, options);
44+
return [{ range, text }];
45+
}
46+
47+
private fullRange(model: monaco.editor.ITextModel): monaco.Range {
48+
const lastLine = model.getLineCount();
49+
const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
50+
const end = new monaco.Position(lastLine, lastLineMaxColumn);
51+
return monaco.Range.fromPositions(new monaco.Position(1, 1), end);
52+
}
53+
54+
/**
55+
* From the currently opened workspaces (IDE2 has always one), it calculates all possible
56+
* folder locations where the `.clang-format` file could be.
57+
*/
58+
private formatterConfigFolderUris(model: monaco.editor.ITextModel): string[] {
59+
const editorUri = new URI(model.uri.toString());
60+
return this.workspaceService
61+
.tryGetRoots()
62+
.map(({ resource }) => resource)
63+
.filter((workspaceUri) => workspaceUri.isEqualOrParent(editorUri))
64+
.map((uri) => uri.toString());
65+
}
66+
67+
private format(
68+
model: monaco.editor.ITextModel,
69+
range: monaco.Range,
70+
options: monaco.languages.FormattingOptions
71+
): Promise<string> {
72+
console.info(
73+
`Formatting ${model.uri.toString()} [Range: ${JSON.stringify(
74+
range.toJSON()
75+
)}]`
76+
);
77+
const content = model.getValueInRange(range);
78+
const formatterConfigFolderUris = this.formatterConfigFolderUris(model);
79+
return this.formatter.format({
80+
content,
81+
formatterConfigFolderUris,
82+
options,
83+
});
84+
}
85+
86+
private selectorOf(
87+
...languageId: string[]
88+
): monaco.languages.LanguageSelector {
89+
return languageId.map((language) => ({
90+
language,
91+
exclusive: true, // <-- this should make sure the custom formatter has higher precedence over the LS formatter.
92+
}));
93+
}
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { injectable } from '@theia/core/shared/inversify';
2+
import { MonacoFormattingConflictsContribution as TheiaMonacoFormattingConflictsContribution } from '@theia/monaco/lib/browser/monaco-formatting-conflicts';
3+
4+
@injectable()
5+
export class MonacoFormattingConflictsContribution extends TheiaMonacoFormattingConflictsContribution {
6+
override async initialize(): Promise<void> {
7+
// NOOP - does not register a custom formatting conflicts selects.
8+
// Does not get and set formatter preferences when selecting from multiple formatters.
9+
// Does not show quick-pick input when multiple formatters are available for the text model.
10+
// Uses the default behavior from VS Code: https://github.com/microsoft/vscode/blob/fb9f488e51af2e2efe95a34f24ca11e1b2a3f744/src/vs/editor/editor.api.ts#L19-L21
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export const FormatterPath = '/services/formatter';
2+
export const Formatter = Symbol('Formatter');
3+
export interface Formatter {
4+
format({
5+
content,
6+
formatterConfigFolderUris,
7+
options,
8+
}: {
9+
content: string;
10+
formatterConfigFolderUris: string[];
11+
options?: FormatterOptions;
12+
}): Promise<string>;
13+
}
14+
export interface FormatterOptions {
15+
/**
16+
* Size of a tab in spaces.
17+
*/
18+
tabSize: number;
19+
/**
20+
* Prefer spaces over tabs.
21+
*/
22+
insertSpaces: boolean;
23+
}

arduino-ide-extension/src/node/arduino-ide-backend-module.ts

+13
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ import WebSocketServiceImpl from './web-socket/web-socket-service-impl';
9494
import { WebSocketService } from './web-socket/web-socket-service';
9595
import { ArduinoLocalizationContribution } from './arduino-localization-contribution';
9696
import { LocalizationContribution } from '@theia/core/lib/node/i18n/localization-contribution';
97+
import { ClangFormatter } from './clang-formatter';
98+
import { FormatterPath } from '../common/protocol/formatter';
9799

98100
export default new ContainerModule((bind, unbind, isBound, rebind) => {
99101
bind(BackendApplication).toSelf().inSingletonScope();
@@ -126,6 +128,17 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
126128
)
127129
.inSingletonScope();
128130

131+
// Shared formatter
132+
bind(ClangFormatter).toSelf().inSingletonScope();
133+
bind(ConnectionHandler)
134+
.toDynamicValue(
135+
({ container }) =>
136+
new JsonRpcConnectionHandler(FormatterPath, () =>
137+
container.get(ClangFormatter)
138+
)
139+
)
140+
.inSingletonScope();
141+
129142
// Examples service. One per backend, each connected FE gets a proxy.
130143
bind(ConnectionContainerModule).toConstantValue(
131144
ConnectionContainerModule.create(({ bind, bindBackendService }) => {

0 commit comments

Comments
 (0)