如何解决编写 cpp 时,Emacs 默认缩进和 clang-format 的冲突

Question

最近在学习 openmp 的代码,发现了 Emacs 默认缩进与 clang-format 风格差别很大。 我使用 super-save + apheleia,会每隔几秒自动格式化。 由于缩进风格的差异,每一次格式化之后,我再编写代码,代码风格又会发生改变, 最让人无法接受的是一段长代码里面,当前编辑的代码和其他的代码风格是不同的, 这给我阅读代码带来了很大的困难。

clang-format 格式化后

#include <iostream>
#include <omp.h>

int main() {
#pragma omp parallel sections
    {
#pragma omp section
        {
            std::cout << "this is section one\n";
#pragma omp parallel for
            for (int i = 0; i < 4; ++i) {
                std::cout << "1 " << i << "\n";
            }
        }
#pragma omp section
        {
            std::cout << "this is section two\n";
#pragma omp parallel for
            for (int i = 0; i < 4; ++i) {
                std::cout << "2 " << i << "\n";
            }
        }
    }
}

Emacs 的默认风格

#include <iostream>
#include <omp.h>

int main() {
#pragma omp parallel sections
{
#pragma omp section
{
    std::cout << "this is section one\n";
#pragma omp parallel for
for (int i = 0; i < 4; ++i) {
    std::cout << "1 " << i << "\n";
}
}
#pragma omp section
{
    std::cout << "this is section two\n";
#pragma omp parallel for
for (int i = 0; i < 4; ++i) {
    std::cout << "2 " << i << "\n";
}
}
}
}

clang-format 格式化后使用 Emacs 编辑后半部分

#include <iostream>
#include <omp.h>

int main() {
#pragma omp parallel sections
    {
#pragma omp section
        {
            std::cout << "this is section one\n";
#pragma omp parallel for
            for (int i = 0; i < 4; ++i) {
                std::cout << "1 " << i << "\n";
            }
        }
#pragma omp section
{
    std::cout << "this is section two\n";
#pragma omp parallel for
for (int i = 0; i < 4; ++i) {
    std::cout << "2 " << i << "\n";
}
}
}
}

Correspond configuration

  • c-default-tyle: ((java-mode . "java") (awk-mode . "awk") (other . "gnu"))
  • .clang-format:
    BasedOnStyle: LLVM
    IndentWidth: 4
    TabWidth: 4
    UseTab: Never
    ColumnLimit: 80
    SortIncludes: true
    IndentPPDirectives: None
    

Others

尝试问过 AI,调整了很久 c-default-type 效果都不理想。感觉想用 c-default-type 来尽可能接近 .clang-format 设定的格式不太现实。

或许我应该放弃如此频繁的格式化,而是在完成一个小的功能之后,编译或者提交代码之前格式化。但是调整一个已经格式化好的代码,Emacs 默认的缩进格式和该代码的格式化风格的差异, 也会让人感到不适。

不知道,论坛里面有没有大佬有比较好的解决方案。


To be continue

经过一定的讨论,我决定不在没有 format 设置的项目里面使用 format 的功能了。 同时找个时间调节一下默认缩进风格,完成之后回来更新。 感谢各位大佬的帮助。

你尝试打开eglot后再看缩进情况。

编辑完成后用clang-format自动排版就行了,clang-format是个代码格式化工具,本来就不是让你写代码的时候换行用的。如果你不喜欢它的换行风格可以写一个.clang-format约束它的行为,如果喜欢那就不要管cc-mode给你自动换行换成什么样了,写两段就用format-all-buffer格式化一下即可。

可以给cc-mode配置自己喜欢的缩进样式, 还挺简单的; 我一般是关闭clangd的自动排版, 在写完代码后再去调用clang-format

自定义 emacs 的缩进配置,适配 clang-format

cc-mode 的配置方法可以参考 我想了解下, emacs 下的缩进到底是怎么设置的...?😥 - #7,来自 xhcoding

emacs 31 下 c+±ts-mode 的配置缩进的方法:

    (treesit-simple-indent-add-rules 'cpp
                                     '(
                                       ((n-p-gp nil nil "namespace_definition") grand-parent 0) ;; 命名空间不缩进
                                       )
                                     :before
                                     'c-ts-common-baseline-indent-rule
                                     )
1 个赞
# .clang-format 
DisableFormat: true

我使用的是 lsp-bridge 补全。之前写 react 前端,lsp-bridge 用起来舒服太多了。

1 个赞

我就是使用在保存的时候才 format 的。我用 format 主要还是觉得代码整齐一点好看。但是修改别人代码的时候,也会 format 造成了大量不必要的 diff。所以我有在认真考虑要不要放弃日常使用 format,改为在特定项目下用 .dir-local.el 启用。

其实我说eglot,是因为eglot打开和不打开的缩进是两种不同情况。

你可以尝试看看这个工具。这个实际并不是真的格式化你的文本,仅仅是看起来格式化文本。

如果一个项目没有相关配置文件或者严格的代码规范,那么最好不要使用自动格式化。日常使用并不妨碍,把保存时自动格式化给改成手动格式化就行了。

明白的。

这个工具好特别啊,不过我不是这种所见非所得的方式。我连 org-mode 都只是开启了,headline 字体变大,以及链接的美化而已。

这个我看过官方文档了,有时间再去配置一下,感谢了。

尝试过了 Emacs 默认的 cc-mode 的调节,结论是完全不可能调节到合适的地步。 因为 cc-mode 使用的是正则匹配,一旦阅读到 #pragma 就会认为是预处理宏, 让下一行的代码缩进顶格。而不是像 clang-format 一样按照代码层次缩进。

才发现可以多重引用,一次性回复,哈哈哈。

我的cc-mode style配置,format前后缩进方面的差别已经很小了。需要自己慢慢调整

(defconst google-c-style
  `((c-recognize-knr-p . nil)
    (c-enable-xemacs-performance-kludge-p . t) ; speed up indentation in XEmacs
    (c-basic-offset . 2)
    (indent-tabs-mode . nil)
    (c-comment-only-line-offset . 0)
    (c-hanging-braces-alist . ((defun-open after)
                               (defun-close before after)
                               (class-open after)
                               (class-close before after)
                               (inexpr-class-open after)
                               (inexpr-class-close before)
                               (namespace-open after)
                               (inline-open after)
                               (inline-close before after)
                               (block-open after)
                               (block-close . c-snug-do-while)
                               (extern-lang-open after)
                               (extern-lang-close after)
                               (statement-case-open after)
                               (substatement-open after)))
    (c-hanging-colons-alist . ((case-label)
                               (label after)
                               (access-label after)
                               (member-init-intro before)
                               (inher-intro)))
    (c-hanging-semi&comma-criteria
     . (c-semi&comma-no-newlines-for-oneline-inliners
        c-semi&comma-inside-parenlist
        c-semi&comma-no-newlines-before-nonblanks))
    (c-indent-comments-syntactically-p . t)
    (comment-column . 40)
    (c-indent-comment-alist . ((other . (space . 2))))
    (c-cleanup-list . (brace-else-brace
                       brace-elseif-brace
                       brace-catch-brace
                       empty-defun-braces
                       defun-close-semi
                       list-close-comma
                       scope-operator))
    (c-offsets-alist . ((func-decl-cont . ++)
                        (member-init-intro . ++)
                        (inher-intro . ++)
                        (comment-intro . 0)
                        (arglist-close . c-lineup-arglist)
                        (topmost-intro . 0)
                        (block-open . 0)
                        (inline-open . 0)
                        (inextern-lang . 0)
                        (substatement-open . 0)
                        (statement-cont
                         .
                         (,(when (fboundp 'c-no-indent-after-java-annotations)
                             'c-no-indent-after-java-annotations)
                          ,(when (fboundp 'c-lineup-assignments)
                             'c-lineup-assignments)
                          ++))
                        (label . /)
                        (statement-case-open . +)
                        (statement-case-intro . +) ; case w/o {
                        (innamespace . 0))))
  "Modified Google C/C++ Programming Style.")

(defconst fdb-c-style
  `(;; IndentWidth: 4
    (c-basic-offset . 4)
    ;; UseTab: ForIndentation
    (indent-tabs-mode . t)
    ;; TabWidth: 4
    (tab-width . 4)
    ;; ColumnLimit: 120
    (fill-column . 120)
    ;; BreakBeforeBraces: Attach (K&R style, e.g., "if (cond) {")
    (c-hanging-braces-alist . ((defun-open after)
                               (defun-close before after)
                               (class-open after)
                               (class-close before after)
                               (namespace-open after)
                               (inline-open after)
                               (inline-close before after)
                               (block-open after)
                               (block-close . c-snug-do-while)
                               (extern-lang-open after)
                               (extern-lang-close after)
                               (statement-case-open after)
                               (substatement-open after)))
    (c-offsets-alist . (
                        ;; AccessModifierOffset: -4
                        (access-label . -4)
                        ;; IndentCaseLabels: false
                        (case-label . 0)
                        ;; NamespaceIndentation: None
                        (innamespace . 0)
                        ;; ConstructorInitializerIndentWidth: 2
                        (member-init-intro . 2)
                        (member-init-cont . 2)
                        ;; IndentWrappedFunctionNames: false
                        (func-decl-cont . 0)
                        ;; AlignAfterOpenBracket: Align
                        (arglist-cont-nonempty . c-lineup-arglist-intro-after-paren)
                        (arglist-cont . c-lineup-arglist-intro-after-paren)
                        (arglist-close . c-lineup-arglist)
                        ;; -- Sensible defaults for C++ --
                        ;; AlwaysBreakTemplateDeclarations: true
                        (topmost-intro . 0)
                        (block-open . 0)
                        (substatement . +)
                        (substatement-open . 0)
                        (statement-case-open . +)
                        (statement-case-intro . +)
                        (inher-intro . +)
                        (inher-cont . c-lineup-multi-inher)
                        (comment-intro . 0)))
    ;; ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ]
    (c-recognize-foreach-p . t)
    (c-foreach-keywords . '("foreach" "Q_FOREACH" "BOOST_FOREACH")))
  "FoundationDB style based on Mozilla.")

(defvar-local my-c-style "google")

(with-eval-after-load "cc-mode"
  (c-add-style "google" google-c-style nil)
  (c-add-style "fdb" fdb-c-style nil)
  (defun my-c-style-setup ()
    (when (or (eq major-mode 'c-mode) (eq major-mode 'c++-mode))
      (c-set-style my-c-style)))
  (add-hook 'hack-local-variables-hook #'my-c-style-setup))

每写完一点代码就 format 一遍,这样就不用改配置了。用 lsp 的话,直接用 lsp 的 format 也一样。你可以加个 save hook 自动调用。

有git-clang-format脚本,只给git add过的区域做format

自动全文件格式化没什么意义, 很多时候同一个规则用在不同的代码段上会出现截然不同的视觉效果, 特别涉及到 line wrapping, 各有不同的丑法。