Skip to content

#1009, #566: Use clang-format as the default sketch formatter. #1019

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -566,6 +570,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 @@ -633,6 +643,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