Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit a59e0da

Browse filesBrowse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
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 #1009 Closes #566 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
1 parent 3a3ac6d commit a59e0da
Copy full SHA for a59e0da

File tree

9 files changed

+461
-3
lines changed
Filter options

9 files changed

+461
-3
lines changed

‎arduino-ide-extension/package.json

Copy file name to clipboardExpand all lines: arduino-ide-extension/package.json
+1-1Lines changed: 1 addition & 1 deletion
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

Copy file name to clipboardExpand all lines: arduino-ide-extension/scripts/download-ls.js
+15-1Lines changed: 15 additions & 1 deletion
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

Copy file name to clipboardExpand all lines: arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
+18Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,10 @@ import { EditorManager } from './theia/editor/editor-manager';
280280
import { HostedPluginEvents } from './hosted-plugin-events';
281281
import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin';
282282
import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
283+
import { Formatter, FormatterPath } from '../common/protocol/formatter';
284+
import { Format } from './contributions/format';
285+
import { MonacoFormattingConflictsContribution } from './theia/monaco/monaco-formatting-conflicts';
286+
import { MonacoFormattingConflictsContribution as TheiaMonacoFormattingConflictsContribution } from '@theia/monaco/lib/browser/monaco-formatting-conflicts';
283287

284288
const ElementQueries = require('css-element-queries/src/ElementQueries');
285289

@@ -573,6 +577,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
573577
)
574578
.inSingletonScope();
575579

580+
bind(Formatter)
581+
.toDynamicValue(({ container }) =>
582+
WebSocketConnectionProvider.createProxy(container, FormatterPath)
583+
)
584+
.inSingletonScope();
585+
576586
bind(ArduinoFirmwareUploader)
577587
.toDynamicValue((context) =>
578588
WebSocketConnectionProvider.createProxy(
@@ -640,6 +650,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
640650
Contribution.configure(bind, ArchiveSketch);
641651
Contribution.configure(bind, AddZipLibrary);
642652
Contribution.configure(bind, PlotterFrontendContribution);
653+
Contribution.configure(bind, Format);
654+
655+
// Disabled the quick-pick customization from Theia when multiple formatters are available.
656+
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.
657+
bind(MonacoFormattingConflictsContribution).toSelf().inSingletonScope();
658+
rebind(TheiaMonacoFormattingConflictsContribution).toService(
659+
MonacoFormattingConflictsContribution
660+
);
643661

644662
bind(ResponseServiceImpl)
645663
.toSelf()
+94Lines changed: 94 additions & 0 deletions
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+
}
+12Lines changed: 12 additions & 0 deletions
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+
}
+23Lines changed: 23 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: arduino-ide-extension/src/node/arduino-ide-backend-module.ts
+13Lines changed: 13 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.