Skip to content

Commit

Permalink
Use clang-format as the default sketch formatter.
Browse files Browse the repository at this point in the history
 - 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 <a.kitta@arduino.cc>
  • Loading branch information
Akos Kitta authored and kittaakos committed Jun 7, 2022
1 parent 3a3ac6d commit a59e0da
Show file tree
Hide file tree
Showing 9 changed files with 461 additions and 3 deletions.
2 changes: 1 addition & 1 deletion arduino-ide-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
"version": "2.0.0"
},
"clangd": {
"version": "13.0.0"
"version": "14.0.0"
},
"languageServer": {
"version": "0.6.0"
Expand Down
16 changes: 15 additions & 1 deletion arduino-ide-extension/scripts/download-ls.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,21 +66,24 @@
build,
`arduino-language-server${platform === 'win32' ? '.exe' : ''}`
);
let clangdExecutablePath, lsSuffix, clangdSuffix;
let clangdExecutablePath, clangFormatExecutablePath, lsSuffix, clangdSuffix;

switch (platformArch) {
case 'darwin-x64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'macOS_64bit.tar.gz';
clangdSuffix = 'macOS_64bit';
break;
case 'linux-x64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'Linux_64bit.tar.gz';
clangdSuffix = 'Linux_64bit';
break;
case 'win32-x64':
clangdExecutablePath = path.join(build, 'clangd.exe');
clangFormatExecutablePath = path.join(build, 'clang-format.exe');
lsSuffix = 'Windows_64bit.zip';
clangdSuffix = 'Windows_64bit';
break;
Expand All @@ -103,4 +106,15 @@
downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, {
strip: 1,
}); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder.

const clangdFormatUrl = `https://downloads.arduino.cc/tools/clang-format_${clangdVersion}_${clangdSuffix}.tar.bz2`;
downloader.downloadUnzipAll(
clangdFormatUrl,
build,
clangFormatExecutablePath,
force,
{
strip: 1,
}
);
})();
18 changes: 18 additions & 0 deletions arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ import { EditorManager } from './theia/editor/editor-manager';
import { HostedPluginEvents } from './hosted-plugin-events';
import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin';
import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { Formatter, FormatterPath } from '../common/protocol/formatter';
import { Format } from './contributions/format';
import { MonacoFormattingConflictsContribution } from './theia/monaco/monaco-formatting-conflicts';
import { MonacoFormattingConflictsContribution as TheiaMonacoFormattingConflictsContribution } from '@theia/monaco/lib/browser/monaco-formatting-conflicts';

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

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

bind(Formatter)
.toDynamicValue(({ container }) =>
WebSocketConnectionProvider.createProxy(container, FormatterPath)
)
.inSingletonScope();

bind(ArduinoFirmwareUploader)
.toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(
Expand Down Expand Up @@ -640,6 +650,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, ArchiveSketch);
Contribution.configure(bind, AddZipLibrary);
Contribution.configure(bind, PlotterFrontendContribution);
Contribution.configure(bind, Format);

// Disabled the quick-pick customization from Theia when multiple formatters are available.
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.
bind(MonacoFormattingConflictsContribution).toSelf().inSingletonScope();
rebind(TheiaMonacoFormattingConflictsContribution).toService(
MonacoFormattingConflictsContribution
);

bind(ResponseServiceImpl)
.toSelf()
Expand Down
94 changes: 94 additions & 0 deletions arduino-ide-extension/src/browser/contributions/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { MaybePromise } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as monaco from '@theia/monaco-editor-core';
import { Formatter } from '../../common/protocol/formatter';
import { Contribution, URI } from './contribution';

@injectable()
export class Format
extends Contribution
implements
monaco.languages.DocumentRangeFormattingEditProvider,
monaco.languages.DocumentFormattingEditProvider
{
@inject(Formatter)
private readonly formatter: Formatter;

override onStart(): MaybePromise<void> {
const selector = this.selectorOf('ino', 'c', 'cpp', 'h', 'hpp', 'pde');
monaco.languages.registerDocumentRangeFormattingEditProvider(
selector,
this
);
monaco.languages.registerDocumentFormattingEditProvider(selector, this);
}
async provideDocumentRangeFormattingEdits(
model: monaco.editor.ITextModel,
range: monaco.Range,
options: monaco.languages.FormattingOptions,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.TextEdit[]> {
const text = await this.format(model, range, options);
return [{ range, text }];
}

async provideDocumentFormattingEdits(
model: monaco.editor.ITextModel,
options: monaco.languages.FormattingOptions,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: monaco.CancellationToken
): Promise<monaco.languages.TextEdit[]> {
const range = this.fullRange(model);
const text = await this.format(model, range, options);
return [{ range, text }];
}

private fullRange(model: monaco.editor.ITextModel): monaco.Range {
const lastLine = model.getLineCount();
const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
const end = new monaco.Position(lastLine, lastLineMaxColumn);
return monaco.Range.fromPositions(new monaco.Position(1, 1), end);
}

/**
* From the currently opened workspaces (IDE2 has always one), it calculates all possible
* folder locations where the `.clang-format` file could be.
*/
private formatterConfigFolderUris(model: monaco.editor.ITextModel): string[] {
const editorUri = new URI(model.uri.toString());
return this.workspaceService
.tryGetRoots()
.map(({ resource }) => resource)
.filter((workspaceUri) => workspaceUri.isEqualOrParent(editorUri))
.map((uri) => uri.toString());
}

private format(
model: monaco.editor.ITextModel,
range: monaco.Range,
options: monaco.languages.FormattingOptions
): Promise<string> {
console.info(
`Formatting ${model.uri.toString()} [Range: ${JSON.stringify(
range.toJSON()
)}]`
);
const content = model.getValueInRange(range);
const formatterConfigFolderUris = this.formatterConfigFolderUris(model);
return this.formatter.format({
content,
formatterConfigFolderUris,
options,
});
}

private selectorOf(
...languageId: string[]
): monaco.languages.LanguageSelector {
return languageId.map((language) => ({
language,
exclusive: true, // <-- this should make sure the custom formatter has higher precedence over the LS formatter.
}));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { injectable } from '@theia/core/shared/inversify';
import { MonacoFormattingConflictsContribution as TheiaMonacoFormattingConflictsContribution } from '@theia/monaco/lib/browser/monaco-formatting-conflicts';

@injectable()
export class MonacoFormattingConflictsContribution extends TheiaMonacoFormattingConflictsContribution {
override async initialize(): Promise<void> {
// NOOP - does not register a custom formatting conflicts selects.
// Does not get and set formatter preferences when selecting from multiple formatters.
// Does not show quick-pick input when multiple formatters are available for the text model.
// Uses the default behavior from VS Code: https://github.com/microsoft/vscode/blob/fb9f488e51af2e2efe95a34f24ca11e1b2a3f744/src/vs/editor/editor.api.ts#L19-L21
}
}
23 changes: 23 additions & 0 deletions arduino-ide-extension/src/common/protocol/formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const FormatterPath = '/services/formatter';
export const Formatter = Symbol('Formatter');
export interface Formatter {
format({
content,
formatterConfigFolderUris,
options,
}: {
content: string;
formatterConfigFolderUris: string[];
options?: FormatterOptions;
}): Promise<string>;
}
export interface FormatterOptions {
/**
* Size of a tab in spaces.
*/
tabSize: number;
/**
* Prefer spaces over tabs.
*/
insertSpaces: boolean;
}
13 changes: 13 additions & 0 deletions arduino-ide-extension/src/node/arduino-ide-backend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ import WebSocketServiceImpl from './web-socket/web-socket-service-impl';
import { WebSocketService } from './web-socket/web-socket-service';
import { ArduinoLocalizationContribution } from './arduino-localization-contribution';
import { LocalizationContribution } from '@theia/core/lib/node/i18n/localization-contribution';
import { ClangFormatter } from './clang-formatter';
import { FormatterPath } from '../common/protocol/formatter';

export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BackendApplication).toSelf().inSingletonScope();
Expand Down Expand Up @@ -126,6 +128,17 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
.inSingletonScope();

// Shared formatter
bind(ClangFormatter).toSelf().inSingletonScope();
bind(ConnectionHandler)
.toDynamicValue(
({ container }) =>
new JsonRpcConnectionHandler(FormatterPath, () =>
container.get(ClangFormatter)
)
)
.inSingletonScope();

// Examples service. One per backend, each connected FE gets a proxy.
bind(ConnectionContainerModule).toConstantValue(
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
Expand Down
Loading

0 comments on commit a59e0da

Please sign in to comment.