├── .gitignore
├── .vscode
├── launch.json
└── tasks.json
├── LICENCE.md
├── README.md
├── front-end
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── activateArrayIcon.svg
│ ├── activeFieldIcon.svg
│ ├── addIcon.svg
│ ├── deactivateArrayIcon.svg
│ ├── deleteIcon.svg
│ ├── favicon.ico
│ ├── favicon.svg
│ ├── inactiveFieldIcon.svg
│ ├── index.html
│ ├── loadingFailedIcon.svg
│ ├── loadingFinishedIcon.svg
│ ├── loadingIcon.gif
│ ├── loadingIcon.svg
│ ├── manifest.json
│ ├── questionIcon.svg
│ └── robots.txt
└── src
│ ├── API.js
│ ├── App.jsx
│ ├── Yaml&ConfigStuff.js
│ ├── code_snippets
│ ├── cppCode.cpp
│ └── javaCode.java
│ ├── components
│ ├── ArraySelector.jsx
│ ├── ConfigUiPage.jsx
│ ├── ConfigUiPage.module.css
│ ├── Editor.jsx
│ ├── Editor.module.css
│ ├── Error.jsx
│ ├── Error.module.css
│ ├── Header.jsx
│ ├── Header.module.css
│ ├── IncludeCategoriesSelector.jsx
│ ├── LoadingIcon.jsx
│ ├── LoadingIcon.module.css
│ ├── MapSelector.jsx
│ ├── Option.jsx
│ ├── Option.module.css
│ ├── OptionList.jsx
│ ├── RawStringFormatsSelector.jsx
│ ├── Selector.module.css
│ ├── SingleSelector.jsx
│ └── ace.css
│ ├── config.json
│ ├── index.css
│ ├── index.js
│ └── reportWebVitals.js
├── llvm
├── .gitignore
├── Dockerfile
├── build-config.py
├── prepare.sh
└── requirements.txt
└── server
├── Dockerfile
├── cmd
└── clang-format-configurator
│ └── main.go
├── config.json
├── debug-script.sh
├── debug.Dockerfile
├── gen-service.sh
├── go.mod
├── go.sum
└── internal
└── app
├── config
└── config.go
├── formatter
└── formatter.go
└── server
├── server.go
├── server_test.go
└── testing_data.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
17 | .vscode/*
18 | !.vscode/launch.json
19 | !.vsode/tasks.json
20 |
21 | server/bin
22 | server/third-party/*
23 | server/*.service
24 |
25 | front-end/.prettierrc
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Golang Remote Debug",
6 | "type": "go",
7 | "request": "attach",
8 | "mode": "remote",
9 | "port": 2345,
10 | "host": "127.0.0.1",
11 | "apiVersion": 2,
12 | "trace": "verbose",
13 | "showLog": true,
14 | "preLaunchTask": "start_server_remote_debugger"
15 | },
16 | {
17 | "name": "Golang Local Debug",
18 | "type": "go",
19 | "request": "attach",
20 | "mode": "remote",
21 | "port": 2345,
22 | "host": "127.0.0.1",
23 | "apiVersion": 2,
24 | "trace": "verbose",
25 | "showLog": true,
26 | "preLaunchTask": "start_server_local_debugger"
27 | },
28 | {
29 | "name": "Debug React App",
30 | "request": "launch",
31 | "type": "chrome",
32 | "preLaunchTask": "start_npm_dev_server",
33 | "url": "http://localhost:3000",
34 | "webRoot": "${workspaceFolder}"
35 | }
36 | ]
37 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "start_server_remote_debugger",
6 | "command": "./debug-script.sh",
7 | "args": [
8 | "--all"
9 | ],
10 | "isBackground": true,
11 | "problemMatcher": {
12 | "owner": "custom",
13 | "pattern": [
14 | {
15 | "regexp": "(.*):(\\d+:\\d+):(.*)",
16 | "file": 1,
17 | "location": 2,
18 | "message": 3
19 | }
20 | ],
21 | "background": {
22 | "activeOnStart": false,
23 | "beginsPattern": "Sending build.*",
24 | "endsPattern": "API server listening at.*"
25 | }
26 | },
27 | "type": "shell",
28 | "options": {
29 | "cwd": "${workspaceFolder}/server/"
30 | }
31 | },
32 | {
33 | "label": "start_server_local_debugger",
34 | "command": "./debug-script.sh",
35 | "args": [
36 | "--run-local"
37 | ],
38 | "type": "shell",
39 | "isBackground": true,
40 | "problemMatcher": {
41 | "owner": "custom",
42 | "pattern": [
43 | {
44 | "regexp": "(.*):(\\d+:\\d+):(.*)",
45 | "file": 1,
46 | "location": 2,
47 | "message": 3
48 | }
49 | ],
50 | "background": {
51 | "activeOnStart": false,
52 | "beginsPattern": "Starting local debugger",
53 | "endsPattern": "API server listening at.*"
54 | }
55 | },
56 | "options": {
57 | "cwd": "${workspaceFolder}/server/"
58 | }
59 | },
60 | {
61 | "label": "stop_container",
62 | "command": "./debug-script.sh",
63 | "args": [
64 | "--stop"
65 | ],
66 | "type": "shell",
67 | "options": {
68 | "cwd": "${workspaceFolder}/server/"
69 | }
70 | },
71 | {
72 | "type": "npm",
73 | "script": "start",
74 | "path": "front-end",
75 | "isBackground": true,
76 | "label": "start_npm_dev_server",
77 | "problemMatcher": {
78 | "owner": "custom",
79 | "pattern": [
80 | {
81 | "regexp": "(.*):(\\d+:\\d+):(.*)",
82 | "file": 1,
83 | "location": 2,
84 | "message": 3
85 | }
86 | ],
87 | "background": {
88 | "activeOnStart": false,
89 | "beginsPattern": "Starting the development server*",
90 | "endsPattern": "compiled"
91 | }
92 | },
93 | "detail": "BROWSER=chrome react-scripts start",
94 | "options": {
95 | "cwd": "${workspaceFolder}/front-end/"
96 | }
97 |
98 | }
99 | ]
100 | }
--------------------------------------------------------------------------------
/LICENCE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Wirena
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Clang-format-configurator-v2
2 |
3 | Interactively create a clang-format configuration while watching how the changes affect your code.
4 |
5 | Check it out at https://clang-format-configurator.site/
6 |
7 | Clang-format-configurator-v2 is a written from scratch successor of [clang-format-configurator](https://github.com/zed0/clang-format-configurator) with following bugs fixed and new features implemented:
8 |
9 | - Newer clang-format versions are available
10 | - Options show default values instead of "Default" when BasedOnStyle is selected
11 | - Support for complex options such as arrays, BraceWrapping, IncludeCategories, RawStringFormats etc
12 | - Readable error description on invalid option value instead of "Bad Request"
13 | - Support for multiple programming languages
14 | - For complex options config file is correctly(?) generated now
15 | - Incomprehensible frontend shitcode and idiotic config generation algorithm
16 | - Fix of critical "Not invented here" bug
17 |
18 | ## User guide
19 |
20 | ### Basic info
21 |
22 | Clang-format tool defaults all unset options to values from selected BasedOnStyle when doing formatting, this configurator follows such behaviour: every time `BasedOnStyle` is changed, all options are filled with values from selected style, when user uploads their .clang-format config file, configurator also automatically fills all unset options to match style from user's config.
23 |
24 | Default values for enum options in dropdown selectors are marked with asterisk (*) on Firefox and with different color and italic font style on other browsers. Default values for strings and numbers are set as a placeholder (can be seen when input field is empty). Setting dropdown selectors to blank does the same thing as setting them to current style's default value.
25 |
26 | String selectors have "Unset" button to the right of an edit field and work this way:
27 |
28 | - Unset option marked with "Option unset" text and is, well, unset. It defaults to selected style
29 | - Active option with empty string in it is set to an empty string
30 |
31 | Array selectors have big red X buttons and big blue + buttons. First ones delete element above them, second ones append new empty element to the end. There are no empty arrays, because clang-format treats them the same way as unset option.
32 |
33 | "Autoformat on changes" checkbox is quite self explanatory, works both for code changes and option changes
34 |
35 | Deprecated options are placed at the bottom of the list and have strikethrough title style
36 |
37 | Red "overridden by X" warning means that this option value is overridden by X option value. Currently, works only for `BraceWrapping` and `SpaceBeforeParensOptions`. If you know other options that override something, please let me know by creating an issue.
38 |
39 | Legacy values (`None, Consecutive, AcrossEmptyLines, AcrossComments, AcrossEmptyLinesAndComments`) for `AlignConsecutive*` are not supported by configurator.
40 |
41 | ### Config File page
42 |
43 | Well, "Upload"/"Download" buttons are for uploading/downloading config file, editor is for editing or copypasting config file contents in-place. "Close" button disgards all changes to config file, "Load Config" buttons applies them.
44 |
45 | "Remove duplicates with BasedOnStyle (Not tested)" checkbox is for removing options, which values match selected BasedOnStyle style. This feature is not properly tested.
46 |
47 | ### Diff mode
48 |
49 | Configurator formats code on the left/top panel, code on the right/bottom panel is left unchanged.
50 |
51 | Btw, if you want to make formatting as close as possible to existing formatted code, then try [clang-unformat](https://github.com/alandefreitas/clang-unformat)
52 |
53 |
54 | ## Build and Development
55 |
56 | ### Build and Run
57 |
58 | Requirements: docker, nodejs, linux or wsl
59 |
60 | 1. Build clang-format binaries and frontend config
61 |
62 | ``llvm/prepare.sh`` script creates debian docker image, builds clang-format from source, dumps styles and copy artifacts outside of container, then launches ``llvm/build-config.py`` script
63 |
64 | Whole process takes around 30 minutes on Intel i5 12600
65 |
66 | ```
67 | cd llvm
68 | ./prepare.sh --all
69 | ```
70 | Clang-format binaries are placed in ``server/third-party/clang-format`` direcotry, docs and defaults are in ``llvm/docs/`` and ``llvm/defaults/`` respectively and config is written into ``llvm/config.json``. ``front-end/src/config.json`` is a soft link to ``llvm/config.json``
71 |
72 | Install JS dependencies
73 |
74 | ```
75 | cd front-end
76 | npm install
77 | ```
78 |
79 | 2. Configure server and client
80 |
81 | - Set URL of Format endpoint in ``front-end/src/config.json`` using ``FormatApiUrl`` key, or keep ``http://localhost:8080/format?`` by default
82 | - Set server bind address in ``server/config.json`` using key ``bind-addr``
83 | - Set path to TLS key and certificate in ``server/config.json`` using keys ``certificate-file`` and ``key-file`` or leave it empty to run plain HTTP server without encryption
84 |
85 | 3. Run and Debug with VS Code
86 |
87 | - Run "Debug React App" to debug front-end
88 | - Run "Golang Remote Debug" to debug server in docker container
89 | - Run "Golang Local Debug" to debug server locally if you have golang installed
90 |
91 | ### Clang-format version list modification
92 |
93 | - List of clang-format versions is written in [TAGS](https://github.com/Wirena/clang-format-configurator-v2/blob/master/llvm/prepare.sh#L146) variable in ``llvm/prepare.sh`` script. This array holds git tag names, tag format must follow this pattern: "llvmorg-x.x.x", because ``get-version-from-tag`` extracts version string from tag name. If you want to support a version, which tag name does not follow this pattern, then modify ``get-version-from-tag`` function. Unless it works as is, idk
94 |
95 | - Run ``llvm/prepare.sh`` to build new version binaries and build frontend config file.
96 |
97 | - Then, manually modify "versions" array in backend config file ``server/config.json``.
98 |
99 | ### Config file format
100 |
101 | Config file format is not documented. It's generated by ``llvm/build-config.py`` script, and this script is utter shitcode. Even I don't understand how it works anymore.
102 |
103 | ## TODOs:
104 | - Preserve comments - [feature](https://github.com/Wirena/clang-format-configurator-v2/issues/21#issuecomment-1567945533)
105 | - Better C++ and Java code examples that show effect of selecting different formatting options
106 | - Code examples for Protobuf, C# and Objective C
107 |
108 |
--------------------------------------------------------------------------------
/front-end/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/front-end/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "front-end",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.1",
7 | "@testing-library/react": "^12.1.2",
8 | "@testing-library/user-event": "^13.5.0",
9 | "ace-builds": "^1.7.1",
10 | "file-saver": "^2.0.5",
11 | "html-webpack-plugin": "^5.5.0",
12 | "lodash": "^4.17.21",
13 | "lodash.clonedeepwith": "^4.5.0",
14 | "re-resizable": "^6.9.1",
15 | "react": "^17.0.2",
16 | "react-ace": "^9.5.0",
17 | "react-collapsible": "^2.8.4",
18 | "react-cookie": "^4.1.1",
19 | "react-dom": "^17.0.2",
20 | "react-scripts": "5.0.0",
21 | "reactjs-popup": "^2.0.5",
22 | "web-vitals": "^2.1.4",
23 | "yaml": "^2.2.2"
24 | },
25 | "scripts": {
26 | "start": "BROWSER=none react-scripts start",
27 | "build": "react-scripts build",
28 | "test": "react-scripts test",
29 | "eject": "react-scripts eject"
30 | },
31 | "eslintConfig": {
32 | "extends": [
33 | "react-app",
34 | "react-app/jest"
35 | ]
36 | },
37 | "browserslist": {
38 | "production": [
39 | ">0.2%",
40 | "not dead",
41 | "not op_mini all"
42 | ],
43 | "development": [
44 | "last 1 chrome version",
45 | "last 1 firefox version",
46 | "last 1 safari version"
47 | ]
48 | },
49 | "devDependencies": {
50 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
51 | "style-loader": "^3.3.1"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/front-end/public/activateArrayIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
98 |
--------------------------------------------------------------------------------
/front-end/public/activeFieldIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
114 |
--------------------------------------------------------------------------------
/front-end/public/addIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
99 |
--------------------------------------------------------------------------------
/front-end/public/deactivateArrayIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
113 |
--------------------------------------------------------------------------------
/front-end/public/deleteIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
104 |
--------------------------------------------------------------------------------
/front-end/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wirena/clang-format-configurator-v2/30a75fe2ddc8392185cce27842c00573b65f8bcd/front-end/public/favicon.ico
--------------------------------------------------------------------------------
/front-end/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
98 |
--------------------------------------------------------------------------------
/front-end/public/inactiveFieldIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
159 |
--------------------------------------------------------------------------------
/front-end/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 | Clang-format configurator
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/front-end/public/loadingFailedIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
65 |
--------------------------------------------------------------------------------
/front-end/public/loadingFinishedIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
54 |
--------------------------------------------------------------------------------
/front-end/public/loadingIcon.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wirena/clang-format-configurator-v2/30a75fe2ddc8392185cce27842c00573b65f8bcd/front-end/public/loadingIcon.gif
--------------------------------------------------------------------------------
/front-end/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/front-end/public/questionIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
88 |
--------------------------------------------------------------------------------
/front-end/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/front-end/src/API.js:
--------------------------------------------------------------------------------
1 | import config from "./config.json";
2 |
3 | export function Format(code, yamlStyle, version, language, onFormat, onError) {
4 | let filext = "";
5 | switch (language) {
6 | case "c_cpp":
7 | filext = ".cpp"
8 | break;
9 | case "java":
10 | filext = ".java"
11 | break
12 | }
13 | const body = { code: code, style: yamlStyle }
14 | fetch(config.FormatApiUrl + new URLSearchParams({
15 | version: version,
16 | filext: filext
17 | }), {
18 | body: JSON.stringify(body),
19 | mode: 'cors',
20 | method: 'POST'
21 | })
22 | .then(response => {
23 | if (response.ok)
24 | response.text().then(onFormat)
25 | else
26 | response.text().then(onError)
27 | }).catch((errorObject) => {onError(errorObject.toString())})
28 | }
--------------------------------------------------------------------------------
/front-end/src/App.jsx:
--------------------------------------------------------------------------------
1 | import Header from "./components/Header";
2 | import OptionList from "./components/OptionList";
3 | import { Resizable } from "re-resizable";
4 | import Editor from "./components/Editor";
5 | import Error from "./components/Error";
6 | import LoadingIcon from "./components/LoadingIcon";
7 | import { useEffect, useState, useRef } from "react";
8 | import config from "./config.json";
9 | import { buildYamlCmdString } from "./Yaml&ConfigStuff"
10 | import { Format } from "./API"
11 | import { useCookies } from "react-cookie";
12 | import Popup from 'reactjs-popup';
13 |
14 | import { debounce } from "lodash";
15 | import ConfigUiPage from "./components/ConfigUiPage";
16 |
17 |
18 | const App = () => {
19 | const [cookie, setCookie] = useCookies([])
20 |
21 | const [configPageOpened, setConfigPageOpened] = useState(false)
22 |
23 | const errorText = useRef("")
24 | const [activeErrorPopup, setActiveErrorPopup] = useState(false)
25 | const [darkThemeActive, setDarkThemeActive] = useState(cookie.theme === "dark" || cookie.theme === "light" ? cookie.theme === "dark" : window.matchMedia("(prefers-color-scheme: dark)").matches)
26 | useEffect(() => { if (darkThemeActive) document.body.className = "dark"; else document.body.className = "" }, [darkThemeActive])
27 | const [options, setOptions] = useState(
28 | { selectedVersion: config.Versions.values[0].arg_val_enum.includes(cookie.version) ? cookie.version : config.Versions.values[0].arg_val_enum[0], BasedOnStyle: undefined });
29 | // code editor text
30 | const [text, setText] = useState("");
31 | // autoupdate formatting on option or code change
32 | const [autoUpdateFormatting, setAutoUpdateFormatting] = useState(true);
33 | const [currentLang, setCurrentLang] = useState("c_cpp")
34 | /*
35 | List of options that were modified manually
36 | Used to ease removing options, which values match default values fro selected style
37 | */
38 | const modifiedOptionTitles = useRef([])
39 | /*
40 | Options object with defaults for selected style set, without user input modifications
41 | */
42 | const unmodifiedOptions = useRef({})
43 |
44 |
45 | function formatCode(options, text, currentLang) {
46 | const yamlStr = buildYamlCmdString(options, config)
47 | Format(text, yamlStr, options.selectedVersion, currentLang, (code) => { if (code !== "") { setText(code); window.loadingIcon.setLoadingState("success") } },
48 | (errortext) => {
49 | window.loadingIcon.setLoadingState("error")
50 | errorText.current = errortext
51 | setActiveErrorPopup(true)
52 | })
53 | };
54 |
55 | useEffect(() => { if (autoUpdateFormatting) { formatCode(options, text, currentLang) } }, [text]);
56 |
57 | const editorTabWidth = (options.TabWidth) ? options.TabWidth : 4;
58 |
59 | // Another useeffect for options because of debouncing
60 | const formatCodeDebounced = useRef(debounce(formatCode, 1000, { leading: false, trailing: true })).current
61 | useEffect(() => {
62 | if (autoUpdateFormatting) {
63 | window.loadingIcon.setLoadingState("loading");
64 | formatCodeDebounced(options, text, currentLang)
65 | }
66 | }, [options]);
67 | return (
68 |
69 |
{ const newDarkThemeStatus = !darkThemeActive; setCookie("theme", newDarkThemeStatus ? "dark" : "light", { path: "/" }); setDarkThemeActive(newDarkThemeStatus) }}
73 | onUpdate={() => { window.loadingIcon.setLoadingState("loading"); formatCode(options, text, currentLang) }}
74 | onConfigFile={() => { setConfigPageOpened(true) }}
75 | onAutoFormatChange={setAutoUpdateFormatting}
76 | />
77 | setActiveErrorPopup(false)}>
85 |
88 |
89 | setConfigPageOpened(false)}
93 | modal={true}
94 | closeOnEscape={false}
95 | closeOnDocumentClick={false}
96 | position="center center"
97 | modifiedOptionTitles={modifiedOptionTitles}
98 | unmodifiedOptions={unmodifiedOptions}
99 |
100 | >
101 | setConfigPageOpened(false)}
103 | onLoaded={({ newOptions, _unmodifiedOptions, _modifiedOptionTitles }) => {
104 | if (newOptions === undefined)
105 | return
106 | modifiedOptionTitles.current = _modifiedOptionTitles
107 | unmodifiedOptions.current = _unmodifiedOptions
108 | setOptions(newOptions)
109 | setConfigPageOpened(false)
110 | }}
111 | onError={(errorDescription) => {
112 | errorText.current = errorDescription
113 | setActiveErrorPopup(true)
114 | }}
115 | darkTheme={darkThemeActive}
116 | options={options}
117 | modifiedOptionTitles={modifiedOptionTitles}
118 | unmodifiedOptions={unmodifiedOptions}
119 |
120 | />
121 |
122 |
123 |
124 |
139 |
140 | { unmodifiedOptions.current = options }}
144 | llvmVersionOption={config.Versions}
145 | onOptionChange={setOptions}
146 | updateModifiedList={(title) => {
147 | if (title === undefined)
148 | modifiedOptionTitles.current = []
149 | else {
150 | if (!modifiedOptionTitles.current.includes(title)) modifiedOptionTitles.current.push(title)
151 | }
152 | }}
153 | />
154 |
155 |
156 |
157 | }
165 | columnLimitLine={options.ColumnLimit ? Number(options.ColumnLimit) : 80}
166 | />
167 |
168 |
169 |
170 |
171 | );
172 | }
173 |
174 |
175 | export default App;
176 |
--------------------------------------------------------------------------------
/front-end/src/Yaml&ConfigStuff.js:
--------------------------------------------------------------------------------
1 | import yaml from "yaml"
2 | import { cloneDeepWith, cloneDeep, isEmpty, isEqual, isNumber, isObject } from "lodash";
3 |
4 | export class ValidationError extends Error { }
5 |
6 |
7 | function mapAlignConsecutive(value, version) {
8 | switch (value) {
9 | case "None":
10 | if (version >= 18)
11 | return {
12 | Enabled: false,
13 | AcrossEmptyLines: false,
14 | AcrossComments: false,
15 | AlignCompound: false,
16 | AlignFunctionPointers: false,
17 | PadOperators: true
18 | }
19 | else
20 | return {
21 | Enabled: false,
22 | AcrossEmptyLines: false,
23 | AcrossComments: false,
24 | AlignCompound: false,
25 | PadOperators: true
26 | }
27 | case "Consecutive":
28 | if (version >= 18)
29 | return {
30 | Enabled: true,
31 | AcrossEmptyLines: false,
32 | AcrossComments: false,
33 | AlignCompound: false,
34 | AlignFunctionPointers: false,
35 | PadOperators: true
36 | }
37 | else
38 | return {
39 | Enabled: true,
40 | AcrossEmptyLines: false,
41 | AcrossComments: false,
42 | AlignCompound: false,
43 | PadOperators: true
44 | }
45 | case "AcrossEmptyLines":
46 | if (version >= 18)
47 | return {
48 | Enabled: true,
49 | AcrossEmptyLines: true,
50 | AcrossComments: false,
51 | AlignCompound: false,
52 | AlignFunctionPointers: false,
53 | PadOperators: true
54 | }
55 | else
56 | return {
57 | Enabled: true,
58 | AcrossEmptyLines: true,
59 | AcrossComments: false,
60 | AlignCompound: false,
61 | PadOperators: true
62 | }
63 | case "AcrossComments":
64 | if (version >= 18)
65 | return {
66 | Enabled: true,
67 | AcrossEmptyLines: false,
68 | AcrossComments: true,
69 | AlignCompound: false,
70 | AlignFunctionPointers: false,
71 | PadOperators: true
72 | }
73 | else return {
74 | Enabled: true,
75 | AcrossEmptyLines: false,
76 | AcrossComments: true,
77 | AlignCompound: false,
78 | PadOperators: true
79 | }
80 | case "AcrossEmptyLinesAndComments":
81 | if (version >= 18)
82 | return {
83 | Enabled: true,
84 | AcrossEmptyLines: true,
85 | AcrossComments: true,
86 | AlignCompound: false,
87 | AlignFunctionPointers: false,
88 | PadOperators: true
89 | }
90 | else
91 | return {
92 | Enabled: true,
93 | AcrossEmptyLines: true,
94 | AcrossComments: true,
95 | AlignCompound: false,
96 | PadOperators: true
97 | }
98 | case "true":
99 | if (version >= 18)
100 | return {
101 | Enabled: true,
102 | AcrossEmptyLines: false,
103 | AcrossComments: false,
104 | AlignCompound: false,
105 | AlignFunctionPointers: false,
106 | PadOperators: true
107 | }
108 | else
109 | return {
110 | Enabled: true,
111 | AcrossEmptyLines: false,
112 | AcrossComments: false,
113 | AlignCompound: false,
114 | PadOperators: true
115 | }
116 | case "false":
117 | if (version >= 18)
118 | return {
119 | Enabled: false,
120 | AcrossEmptyLines: false,
121 | AcrossComments: false,
122 | AlignCompound: false,
123 | AlignFunctionPointers: false,
124 | PadOperators: true
125 | }
126 | else
127 | return {
128 | Enabled: false,
129 | AcrossEmptyLines: false,
130 | AcrossComments: false,
131 | AlignCompound: false,
132 | PadOperators: true
133 | }
134 | default:
135 | return undefined
136 | }
137 | }
138 |
139 |
140 |
141 | export function convertLegacyAlignConsectutiveOptions(yamlString, version) {
142 | let options = yaml.parse(yamlString)
143 | const optionsList = Object.keys(options)
144 | for (let i = 0; i < optionsList.length; i++) {
145 | const isString = typeof options[optionsList[i]] === "string";
146 | const isBoolean = typeof options[optionsList[i]] === "boolean";
147 | if (optionsList[i].startsWith("AlignConsecutive") && (isBoolean || isString)) {
148 | const currentVal = options[optionsList[i]];
149 | options[optionsList[i]] = mapAlignConsecutive(isString ? currentVal : currentVal.toString(), version)
150 | }
151 | }
152 | const doc = new yaml.Document();
153 | doc.contents = options
154 | const YamlOptions = {
155 | directives: true
156 | }
157 | return doc.toString(YamlOptions)
158 | }
159 |
160 | function removeOptionsDuplicatingStyleDefs(options, modifiedOptionTitles, unmodifiedOptions) {
161 | for (const [key1, value1] of Object.entries(options)) {
162 |
163 | if (key1 === "BasedOnStyle") continue
164 |
165 | if (key1 === "BraceWrapping") {
166 | if (options["BreakBeforeBraces"] !== "Custom") {
167 | delete options[key1]
168 | }
169 | continue
170 | }
171 |
172 | if (!modifiedOptionTitles.includes(key1)) {
173 | delete options[key1]
174 | continue
175 | }
176 |
177 | if (Array.isArray(value1)) {
178 | if (isEqual(value1, unmodifiedOptions[key1]))
179 | delete options[key1]
180 | continue
181 | }
182 |
183 | // if object, compare its properties and delete those that match unmodified defaults
184 | if (isObject(value1)) {
185 | for (const [key2, value2] of Object.entries(value1))
186 | if (value2 === unmodifiedOptions[key1][key2])
187 | delete options[key1][key2]
188 | if (Object.entries(options[key1]).length === 0)
189 | delete options[key1]
190 | continue
191 | }
192 |
193 | if (value1 === unmodifiedOptions[key1])
194 | delete options[key1]
195 | }
196 | return options
197 | }
198 |
199 |
200 |
201 | export function buildYamlConfigFile(chosenOptions, removeDuplicates, modifiedOptionTitles, unmodifiedOptions) {
202 | let options = cloneDeep(chosenOptions)
203 | if (options.BasedOnStyle !== undefined && removeDuplicates)
204 | options = removeOptionsDuplicatingStyleDefs(options, modifiedOptionTitles, unmodifiedOptions)
205 |
206 | delete options.selectedVersion
207 | const doc = new yaml.Document();
208 | doc.contents = options
209 | const YamlOptions = {
210 | directives: true
211 | }
212 | return doc.toString(YamlOptions)
213 | }
214 |
215 |
216 |
217 | function removeEmpty(obj) {
218 | Object.entries(obj).filter(([k, v]) => isObject(v)).forEach(([k, v]) => {
219 | if (Array.isArray(v)) {
220 | //remove empty objects from array
221 | obj[k] = v.filter(n => Object.entries(Object.fromEntries(
222 | Object.entries(n).filter(([_, v]) => v != null))).length)
223 | if (obj[k].length === 0)
224 | delete obj[k]
225 | } else if (isEmpty(v)) obj[k] = undefined; else v = removeEmpty(v)
226 | })
227 | return obj
228 | }
229 |
230 |
231 |
232 | export function buildYamlCmdString(chosenOptions, config) {
233 | //clone object not to interfere with App's state
234 | const customizer = (value, key, object) => {
235 | if (object) {
236 | /*
237 | This part is kinda cringe: clang-format cant parse flow style yaml file
238 | when integer value is not followed by comma, i.e. any last integer value will give an error,
239 | Example:
240 | {
241 | Regex: '^',
242 | Priority: 2,
243 | SortPriority: 0, <--- OK
244 | CaseSensitive: 'false'
245 | },
246 | {
247 | Regex: '^<.*\.h>',
248 | Priority: '1',
249 | SortPriority: 0 <---- Apparently not OK
250 | },
251 |
252 |
253 | So we turn all number into strings. Genius
254 | */
255 | if (isNumber(value))
256 | return value.toString();
257 |
258 | // And also remove empty objects. Clang-format does not like them either
259 | if (isEmpty(value) && isObject(value))
260 | return null
261 | }
262 | return undefined;
263 | }
264 |
265 | const doc = new yaml.Document();
266 | doc.contents = removeEmpty(cloneDeepWith(chosenOptions, customizer))
267 | delete doc.contents.selectedVersion
268 | const YamlOptions = {
269 | collectionStyle: 'flow', indentSeq: false,
270 | // false, true and other values have to be strings for the same reason as numbers
271 | falseStr: "'false'", trueStr: "'true'", defaultStringType: 'QUOTE_SINGLE',
272 | defaultKeyType: 'PLAIN',
273 | }
274 | return doc.toString(YamlOptions)
275 | }
276 |
277 |
278 | export function loadOptionsFromString(optionsStr, config, selectedVersion, onLoaded) {
279 | const options = yaml.parse(optionsStr)
280 | if (typeof (options) == "string" || Array.isArray(options) || options === null)
281 | throw new Error("Looks like invalid yaml file")
282 |
283 | manuallyValidate(options, selectedVersion)
284 |
285 | const BasedOnStyle = options.BasedOnStyle
286 | if (BasedOnStyle === undefined) {
287 | const modifiedOptionTitles = Object.entries(options).map(([k, v]) => { return k })
288 | options.selectedVersion = selectedVersion
289 | onLoaded({
290 | newOptions: options, _unmodifiedOptions: undefined,
291 | _modifiedOptionTitles: modifiedOptionTitles
292 | })
293 | return
294 | }
295 |
296 | // check if config is compatible with selected version
297 | // values might also differ for same key from version to version but this solution is ok for now
298 |
299 | const listOfOptionTitles = config[selectedVersion].map(el => el.title)
300 | Object.entries(options).forEach(([k, v]) => {
301 | if (!listOfOptionTitles.includes(k)) {
302 | throw new Error("Config contains key that is incompatible with selected clang-format version:\n" + k)
303 | }
304 | })
305 |
306 | // fill optionsWithDefaults with default values for selected style
307 | let unmodifiedOptions = {
308 | selectedVersion: selectedVersion,
309 | BasedOnStyle: BasedOnStyle
310 | }
311 |
312 | config[unmodifiedOptions.selectedVersion]
313 | .slice(1).forEach((option) => {
314 | if (
315 | option.values.length === 1 &&
316 | option.values[0].defaults[BasedOnStyle] !== undefined
317 | ) {
318 | unmodifiedOptions[option.title] =
319 | option.values[0].defaults[BasedOnStyle].value;
320 | } else {
321 | // set all option values to selected style defaults
322 | // inluding nested ones
323 | // filter out options without defaults for this style
324 | option.values.filter((element) => element.defaults[BasedOnStyle] !== undefined)
325 | .forEach((nestedOption) => {
326 | if (unmodifiedOptions[option.title] == undefined)
327 | unmodifiedOptions[option.title] = {}
328 | unmodifiedOptions[option.title][nestedOption.title] =
329 | nestedOption.defaults[BasedOnStyle].value
330 | }
331 | );
332 | }
333 | });
334 | let modifedOptions = cloneDeep(unmodifiedOptions)
335 | let modifiedOptionTitles = []
336 | Object.entries(options).forEach(([k, v]) => {
337 | if (isObject(modifedOptions[k]))
338 | /*if value is object such as BraceWrapping i.e. has nested options
339 | then simply doing modifedOptions[k] = v; will undefine those properties
340 | which were not declared in user selected .clang-format file
341 | */
342 | Object.assign(modifedOptions[k], v)
343 | else
344 | modifedOptions[k] = v;
345 | modifiedOptionTitles.push(k)
346 | })
347 | onLoaded({
348 | newOptions: modifedOptions, _unmodifiedOptions: unmodifiedOptions,
349 | _modifiedOptionTitles: modifiedOptionTitles
350 | })
351 |
352 | }
353 |
354 |
355 |
356 | export function loadOptionsFromFile(fileName, config, selectedVersion, onError, onLoaded) {
357 | const reader = new FileReader();
358 | reader.onload = (e) => {
359 | try {
360 | const options = yaml.parse(e.target.result)
361 | if (typeof (options) == "string")
362 | throw new Error("Looks like invalid yaml file")
363 | const BasedOnStyle = options.BasedOnStyle
364 |
365 | if (BasedOnStyle === undefined) {
366 | const modifiedOptionTitles = Object.entries(options).map(([k, v]) => { return k })
367 | options.selectedVersion = selectedVersion
368 | onLoaded({
369 | newOptions: options, _unmodifiedOptions: undefined,
370 | _modifiedOptionTitles: modifiedOptionTitles
371 | })
372 | return
373 | }
374 |
375 | // check if config is compatible with selected version
376 | // values might also differ for same key from version to version but this solution is ok for now
377 |
378 | const listOfOptionTitles = config[selectedVersion].map(el => el.title)
379 | Object.entries(options).forEach(([k, v]) => {
380 | if (!listOfOptionTitles.includes(k)) {
381 | throw new Error("Config contains key that is incompatible with selected clang-format version:\n" + k)
382 | }
383 | })
384 |
385 | // fill optionsWithDefaults with default values for selected style
386 | let unmodifiedOptions = {
387 | selectedVersion: selectedVersion,
388 | BasedOnStyle: BasedOnStyle
389 | }
390 |
391 | config[unmodifiedOptions.selectedVersion]
392 | .slice(1).forEach((option) => {
393 | if (
394 | option.values.length === 1 &&
395 | option.values[0].defaults[BasedOnStyle] !== undefined
396 | ) {
397 | unmodifiedOptions[option.title] =
398 | option.values[0].defaults[BasedOnStyle].value;
399 | } else {
400 | // set all option values to selected style defaults
401 | // inluding nested ones
402 | // filter out options without defaults for this style
403 | option.values.filter((element) => element.defaults[BasedOnStyle] !== undefined)
404 | .forEach((nestedOption) => {
405 | if (unmodifiedOptions[option.title] == undefined)
406 | unmodifiedOptions[option.title] = {}
407 | unmodifiedOptions[option.title][nestedOption.title] =
408 | nestedOption.defaults[BasedOnStyle].value
409 | }
410 | );
411 | }
412 | });
413 | let modifedOptions = cloneDeep(unmodifiedOptions)
414 | let modifiedOptionTitles = []
415 | Object.entries(options).forEach(([k, v]) => {
416 | if (isObject(modifedOptions[k]))
417 | /*if value is object such as BraceWrapping i.e. has nested options
418 | then simply doing modifedOptions[k] = v; will undefine those properties
419 | which were not declared in user selected .clang-format file
420 | */
421 | Object.assign(modifedOptions[k], v)
422 | else
423 | modifedOptions[k] = v;
424 | modifiedOptionTitles.push(k)
425 | })
426 | onLoaded({
427 | newOptions: modifedOptions, _unmodifiedOptions: unmodifiedOptions,
428 | _modifiedOptionTitles: modifiedOptionTitles
429 | })
430 | } catch (e) {
431 | onError(e.message)
432 | }
433 | }
434 | reader.readAsText(fileName)
435 | }
436 |
437 |
438 |
439 | export function manuallyValidate(config, version) {
440 | // Check AlignConsecutive legacy values
441 | if (parseInt(version) >= 15) {
442 | const optionsList = Object.keys(config)
443 | for (let i = 0; i < optionsList.length; i++) {
444 | const isString = typeof config[optionsList[i]] === "string";
445 | const isBoolean = typeof config[optionsList[i]] === "boolean";
446 | if (optionsList[i].startsWith("AlignConsecutive") && (isBoolean || isString)) {
447 | throw new ValidationError()
448 | }
449 | }
450 | }
451 | }
452 |
453 |
--------------------------------------------------------------------------------
/front-end/src/code_snippets/cppCode.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | // IndentPPDirectives
3 | #ifdef WIN32
4 | #include
5 | #endif
6 | // SortIncludes
7 | #include "AnotherHeader.h"
8 | #include "MyHeader.h"
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include