| 1 | ;;; cmake-mode.el --- major-mode for editing CMake sources |
| 2 | |
| 3 | ;; Package-Requires: ((emacs "24.1")) |
| 4 | |
| 5 | ; Distributed under the OSI-approved BSD 3-Clause License. See accompanying |
| 6 | ; file Copyright.txt or https://cmake.org/licensing for details. |
| 7 | |
| 8 | ;------------------------------------------------------------------------------ |
| 9 | |
| 10 | ;;; Commentary: |
| 11 | |
| 12 | ;; Provides syntax highlighting and indentation for CMakeLists.txt and |
| 13 | ;; *.cmake source files. |
| 14 | ;; |
| 15 | ;; Add this code to your .emacs file to use the mode: |
| 16 | ;; |
| 17 | ;; (setq load-path (cons (expand-file-name "/dir/with/cmake-mode") load-path)) |
| 18 | ;; (require 'cmake-mode) |
| 19 | |
| 20 | ;------------------------------------------------------------------------------ |
| 21 | |
| 22 | ;;; Code: |
| 23 | ;; |
| 24 | ;; cmake executable variable used to run cmake --help-command |
| 25 | ;; on commands in cmake-mode |
| 26 | ;; |
| 27 | ;; cmake-command-help Written by James Bigler |
| 28 | ;; |
| 29 | |
| 30 | (require 'rst) |
| 31 | (require 'rx) |
| 32 | |
| 33 | (defcustom cmake-mode-cmake-executable "cmake" |
| 34 | "*The name of the cmake executable. |
| 35 | |
| 36 | This can be either absolute or looked up in $PATH. You can also |
| 37 | set the path with these commands: |
| 38 | (setenv \"PATH\" (concat (getenv \"PATH\") \";C:\\\\Program Files\\\\CMake 2.8\\\\bin\")) |
| 39 | (setenv \"PATH\" (concat (getenv \"PATH\") \":/usr/local/cmake/bin\"))" |
| 40 | :type 'file |
| 41 | :group 'cmake) |
| 42 | |
| 43 | ;; Keywords |
| 44 | (defconst cmake-keywords-block-open '("IF" "MACRO" "FOREACH" "ELSE" "ELSEIF" "WHILE" "FUNCTION")) |
| 45 | (defconst cmake-keywords-block-close '("ENDIF" "ENDFOREACH" "ENDMACRO" "ELSE" "ELSEIF" "ENDWHILE" "ENDFUNCTION")) |
| 46 | (defconst cmake-keywords |
| 47 | (let ((kwds (append cmake-keywords-block-open cmake-keywords-block-close nil))) |
| 48 | (delete-dups kwds))) |
| 49 | |
| 50 | ;; Regular expressions used by line indentation function. |
| 51 | ;; |
| 52 | (defconst cmake-regex-blank "^[ \t]*$") |
| 53 | (defconst cmake-regex-comment "#.*") |
| 54 | (defconst cmake-regex-paren-left "(") |
| 55 | (defconst cmake-regex-paren-right ")") |
| 56 | (defconst cmake-regex-closing-parens-line (concat "^[[:space:]]*\\(" |
| 57 | cmake-regex-paren-right |
| 58 | "+\\)[[:space:]]*$")) |
| 59 | (defconst cmake-regex-argument-quoted |
| 60 | (rx ?\" (* (or (not (any ?\" ?\\)) (and ?\\ anything))) ?\")) |
| 61 | (defconst cmake-regex-argument-unquoted |
| 62 | (rx (or (not (any space "()#\"\\\n")) (and ?\\ nonl)) |
| 63 | (* (or (not (any space "()#\\\n")) (and ?\\ nonl))))) |
| 64 | (defconst cmake-regex-token |
| 65 | (rx-to-string `(group (or (regexp ,cmake-regex-comment) |
| 66 | ?\( ?\) |
| 67 | (regexp ,cmake-regex-argument-unquoted) |
| 68 | (regexp ,cmake-regex-argument-quoted))))) |
| 69 | (defconst cmake-regex-indented |
| 70 | (rx-to-string `(and bol (* (group (or (regexp ,cmake-regex-token) (any space ?\n))))))) |
| 71 | (defconst cmake-regex-block-open |
| 72 | (rx-to-string `(and symbol-start (or ,@(append cmake-keywords-block-open |
| 73 | (mapcar 'downcase cmake-keywords-block-open))) symbol-end))) |
| 74 | (defconst cmake-regex-block-close |
| 75 | (rx-to-string `(and symbol-start (or ,@(append cmake-keywords-block-close |
| 76 | (mapcar 'downcase cmake-keywords-block-close))) symbol-end))) |
| 77 | (defconst cmake-regex-close |
| 78 | (rx-to-string `(and bol (* space) (regexp ,cmake-regex-block-close) |
| 79 | (* space) (regexp ,cmake-regex-paren-left)))) |
| 80 | (defconst cmake-regex-token-paren-left (concat "^" cmake-regex-paren-left "$")) |
| 81 | (defconst cmake-regex-token-paren-right (concat "^" cmake-regex-paren-right "$")) |
| 82 | |
| 83 | ;------------------------------------------------------------------------------ |
| 84 | |
| 85 | ;; Line indentation helper functions |
| 86 | |
| 87 | (defun cmake-line-starts-inside-string () |
| 88 | "Determine whether the beginning of the current line is in a string." |
| 89 | (save-excursion |
| 90 | (beginning-of-line) |
| 91 | (let ((parse-end (point))) |
| 92 | (goto-char (point-min)) |
| 93 | (nth 3 (parse-partial-sexp (point) parse-end)) |
| 94 | ) |
| 95 | ) |
| 96 | ) |
| 97 | |
| 98 | (defun cmake-find-last-indented-line () |
| 99 | "Move to the beginning of the last line that has meaningful indentation." |
| 100 | (let ((point-start (point)) |
| 101 | region) |
| 102 | (forward-line -1) |
| 103 | (setq region (buffer-substring-no-properties (point) point-start)) |
| 104 | (while (and (not (bobp)) |
| 105 | (or (looking-at cmake-regex-blank) |
| 106 | (cmake-line-starts-inside-string) |
| 107 | (not (and (string-match cmake-regex-indented region) |
| 108 | (= (length region) (match-end 0)))))) |
| 109 | (forward-line -1) |
| 110 | (setq region (buffer-substring-no-properties (point) point-start)) |
| 111 | ) |
| 112 | ) |
| 113 | ) |
| 114 | |
| 115 | ;------------------------------------------------------------------------------ |
| 116 | |
| 117 | ;; |
| 118 | ;; Indentation increment. |
| 119 | ;; |
| 120 | (defcustom cmake-tab-width 2 |
| 121 | "Number of columns to indent cmake blocks" |
| 122 | :type 'integer |
| 123 | :group 'cmake) |
| 124 | |
| 125 | ;; |
| 126 | ;; Line indentation function. |
| 127 | ;; |
| 128 | (defun cmake-indent () |
| 129 | "Indent current line as CMake code." |
| 130 | (interactive) |
| 131 | (unless (cmake-line-starts-inside-string) |
| 132 | (if (bobp) |
| 133 | (cmake-indent-line-to 0) |
| 134 | (let (cur-indent) |
| 135 | (save-excursion |
| 136 | (beginning-of-line) |
| 137 | (let ((point-start (point)) |
| 138 | (closing-parens-only (looking-at cmake-regex-closing-parens-line)) |
| 139 | (case-fold-search t) ;; case-insensitive |
| 140 | token) |
| 141 | ;; Search back for the last indented line. |
| 142 | (cmake-find-last-indented-line) |
| 143 | ;; Start with the indentation on this line. |
| 144 | (setq cur-indent (current-indentation)) |
| 145 | (if closing-parens-only |
| 146 | (let ((open-parens 0)) |
| 147 | (while (re-search-forward cmake-regex-token point-start t) |
| 148 | (setq token (match-string 0)) |
| 149 | (cond |
| 150 | ((string-match cmake-regex-token-paren-left token) |
| 151 | (setq open-parens (+ open-parens 1))) |
| 152 | ((string-match cmake-regex-token-paren-right token) |
| 153 | (setq open-parens (- open-parens 1))))) |
| 154 | ;; Don't outdent if last indented line has open parens |
| 155 | (unless (> open-parens 0) |
| 156 | (setq cur-indent (- cur-indent cmake-tab-width)))) |
| 157 | ;; Skip detailed analysis if last indented line is a 'closing |
| 158 | ;; parens only line' |
| 159 | (unless (looking-at cmake-regex-closing-parens-line) |
| 160 | ;; Search forward counting tokens that adjust indentation. |
| 161 | (while (re-search-forward cmake-regex-token point-start t) |
| 162 | (setq token (match-string 0)) |
| 163 | (when (or (string-match cmake-regex-token-paren-left token) |
| 164 | (and (string-match cmake-regex-block-open token) |
| 165 | (looking-at (concat "[ \t]*" cmake-regex-paren-left)))) |
| 166 | (setq cur-indent (+ cur-indent cmake-tab-width))) |
| 167 | (when (string-match cmake-regex-token-paren-right token) |
| 168 | (setq cur-indent (- cur-indent cmake-tab-width))) |
| 169 | )) |
| 170 | (goto-char point-start) |
| 171 | ;; If next token closes the block, decrease indentation |
| 172 | (when (looking-at cmake-regex-close) |
| 173 | (setq cur-indent (- cur-indent cmake-tab-width)) |
| 174 | ) |
| 175 | ) |
| 176 | ) |
| 177 | ) |
| 178 | ;; Indent this line by the amount selected. |
| 179 | (cmake-indent-line-to (max cur-indent 0)) |
| 180 | ) |
| 181 | ) |
| 182 | ) |
| 183 | ) |
| 184 | |
| 185 | (defun cmake-point-in-indendation () |
| 186 | (string-match "^[ \\t]*$" (buffer-substring (point-at-bol) (point)))) |
| 187 | |
| 188 | (defun cmake-indent-line-to (column) |
| 189 | "Indent the current line to COLUMN. |
| 190 | If point is within the existing indentation it is moved to the end of |
| 191 | the indentation. Otherwise it retains the same position on the line" |
| 192 | (if (cmake-point-in-indendation) |
| 193 | (indent-line-to column) |
| 194 | (save-excursion (indent-line-to column)))) |
| 195 | |
| 196 | ;------------------------------------------------------------------------------ |
| 197 | |
| 198 | ;; |
| 199 | ;; Helper functions for buffer |
| 200 | ;; |
| 201 | (defun cmake-unscreamify-buffer () |
| 202 | "Convert all CMake commands to lowercase in buffer." |
| 203 | (interactive) |
| 204 | (save-excursion |
| 205 | (goto-char (point-min)) |
| 206 | (while (re-search-forward "^\\([ \t]*\\)\\_<\\(\\(?:\\w\\|\\s_\\)+\\)\\_>\\([ \t]*(\\)" nil t) |
| 207 | (replace-match |
| 208 | (concat |
| 209 | (match-string 1) |
| 210 | (downcase (match-string 2)) |
| 211 | (match-string 3)) |
| 212 | t)) |
| 213 | ) |
| 214 | ) |
| 215 | |
| 216 | |
| 217 | ;------------------------------------------------------------------------------ |
| 218 | |
| 219 | ;; |
| 220 | ;; Navigation / marking by function or macro |
| 221 | ;; |
| 222 | |
| 223 | (defconst cmake--regex-defun-start |
| 224 | (rx line-start |
| 225 | (zero-or-more space) |
| 226 | (or "function" "macro") |
| 227 | (zero-or-more space) |
| 228 | "(")) |
| 229 | |
| 230 | (defconst cmake--regex-defun-end |
| 231 | (rx line-start |
| 232 | (zero-or-more space) |
| 233 | "end" |
| 234 | (or "function" "macro") |
| 235 | (zero-or-more space) |
| 236 | "(" (zero-or-more (not-char ")")) ")")) |
| 237 | |
| 238 | (defun cmake-beginning-of-defun () |
| 239 | "Move backward to the beginning of a CMake function or macro. |
| 240 | |
| 241 | Return t unless search stops due to beginning of buffer." |
| 242 | (interactive) |
| 243 | (when (not (region-active-p)) |
| 244 | (push-mark)) |
| 245 | (let ((case-fold-search t)) |
| 246 | (when (re-search-backward cmake--regex-defun-start nil 'move) |
| 247 | t))) |
| 248 | |
| 249 | (defun cmake-end-of-defun () |
| 250 | "Move forward to the end of a CMake function or macro. |
| 251 | |
| 252 | Return t unless search stops due to end of buffer." |
| 253 | (interactive) |
| 254 | (when (not (region-active-p)) |
| 255 | (push-mark)) |
| 256 | (let ((case-fold-search t)) |
| 257 | (when (re-search-forward cmake--regex-defun-end nil 'move) |
| 258 | (forward-line) |
| 259 | t))) |
| 260 | |
| 261 | (defun cmake-mark-defun () |
| 262 | "Mark the current CMake function or macro. |
| 263 | |
| 264 | This puts the mark at the end, and point at the beginning." |
| 265 | (interactive) |
| 266 | (cmake-end-of-defun) |
| 267 | (push-mark nil :nomsg :activate) |
| 268 | (cmake-beginning-of-defun)) |
| 269 | |
| 270 | |
| 271 | ;------------------------------------------------------------------------------ |
| 272 | |
| 273 | ;; |
| 274 | ;; Keyword highlighting regex-to-face map. |
| 275 | ;; |
| 276 | (defconst cmake-font-lock-keywords |
| 277 | `((,(rx-to-string `(and symbol-start |
| 278 | (or ,@cmake-keywords |
| 279 | ,@(mapcar #'downcase cmake-keywords)) |
| 280 | symbol-end)) |
| 281 | . font-lock-keyword-face) |
| 282 | (,(rx symbol-start (group (+ (or word (syntax symbol)))) (* blank) ?\() |
| 283 | 1 font-lock-function-name-face) |
| 284 | (,(rx "${" (group (+(any alnum "-_+/."))) "}") |
| 285 | 1 font-lock-variable-name-face t) |
| 286 | ) |
| 287 | "Highlighting expressions for CMake mode.") |
| 288 | |
| 289 | ;------------------------------------------------------------------------------ |
| 290 | |
| 291 | ;; Syntax table for this mode. |
| 292 | (defvar cmake-mode-syntax-table nil |
| 293 | "Syntax table for CMake mode.") |
| 294 | (or cmake-mode-syntax-table |
| 295 | (setq cmake-mode-syntax-table |
| 296 | (let ((table (make-syntax-table))) |
| 297 | (modify-syntax-entry ?\( "()" table) |
| 298 | (modify-syntax-entry ?\) ")(" table) |
| 299 | (modify-syntax-entry ?# "<" table) |
| 300 | (modify-syntax-entry ?\n ">" table) |
| 301 | (modify-syntax-entry ?$ "'" table) |
| 302 | table))) |
| 303 | |
| 304 | ;; |
| 305 | ;; User hook entry point. |
| 306 | ;; |
| 307 | (defvar cmake-mode-hook nil) |
| 308 | |
| 309 | ;;------------------------------------------------------------------------------ |
| 310 | ;; Mode definition. |
| 311 | ;; |
| 312 | ;;;###autoload |
| 313 | (define-derived-mode cmake-mode prog-mode "CMake" |
| 314 | "Major mode for editing CMake source files." |
| 315 | |
| 316 | ; Setup font-lock mode. |
| 317 | (set (make-local-variable 'font-lock-defaults) '(cmake-font-lock-keywords)) |
| 318 | ; Setup indentation function. |
| 319 | (set (make-local-variable 'indent-line-function) 'cmake-indent) |
| 320 | ; Setup comment syntax. |
| 321 | (set (make-local-variable 'comment-start) "#")) |
| 322 | |
| 323 | ;; Default cmake-mode key bindings |
| 324 | (define-key cmake-mode-map "\e\C-a" #'cmake-beginning-of-defun) |
| 325 | (define-key cmake-mode-map "\e\C-e" #'cmake-end-of-defun) |
| 326 | (define-key cmake-mode-map "\e\C-h" #'cmake-mark-defun) |
| 327 | |
| 328 | |
| 329 | ; Help mode starts here |
| 330 | |
| 331 | |
| 332 | ;;;###autoload |
| 333 | (defun cmake-command-run (type &optional topic buffer) |
| 334 | "Runs the command cmake with the arguments specified. The |
| 335 | optional argument topic will be appended to the argument list." |
| 336 | (interactive "s") |
| 337 | (let* ((bufname (if buffer buffer (concat "*CMake" type (if topic "-") topic "*"))) |
| 338 | (buffer (if (get-buffer bufname) (get-buffer bufname) (generate-new-buffer bufname))) |
| 339 | (command (concat cmake-mode-cmake-executable " " type " " topic)) |
| 340 | ;; Turn of resizing of mini-windows for shell-command. |
| 341 | (resize-mini-windows nil) |
| 342 | ) |
| 343 | (shell-command command buffer) |
| 344 | (save-selected-window |
| 345 | (select-window (display-buffer buffer 'not-this-window)) |
| 346 | (cmake-mode) |
| 347 | (read-only-mode 1) |
| 348 | (view-mode 1)) |
| 349 | ) |
| 350 | ) |
| 351 | |
| 352 | ;;;###autoload |
| 353 | (defun cmake-command-run-help (type &optional topic buffer) |
| 354 | "`cmake-command-run' but rendered in `rst-mode'." |
| 355 | (interactive "s") |
| 356 | (let* ((bufname (if buffer buffer (concat "*CMake" type (if topic "-") topic "*"))) |
| 357 | (buffer (if (get-buffer bufname) (get-buffer bufname) (generate-new-buffer bufname))) |
| 358 | (command (concat cmake-mode-cmake-executable " " type " " topic)) |
| 359 | ;; Turn of resizing of mini-windows for shell-command. |
| 360 | (resize-mini-windows nil) |
| 361 | ) |
| 362 | (shell-command command buffer) |
| 363 | (save-selected-window |
| 364 | (select-window (display-buffer buffer 'not-this-window)) |
| 365 | (rst-mode) |
| 366 | (read-only-mode 1) |
| 367 | (view-mode 1)) |
| 368 | ) |
| 369 | ) |
| 370 | |
| 371 | ;;;###autoload |
| 372 | (defun cmake-help-list-commands () |
| 373 | "Prints out a list of the cmake commands." |
| 374 | (interactive) |
| 375 | (cmake-command-run-help "--help-command-list") |
| 376 | ) |
| 377 | |
| 378 | (defvar cmake-commands '() "List of available topics for --help-command.") |
| 379 | (defvar cmake-help-command-history nil "Command read history.") |
| 380 | (defvar cmake-modules '() "List of available topics for --help-module.") |
| 381 | (defvar cmake-help-module-history nil "Module read history.") |
| 382 | (defvar cmake-variables '() "List of available topics for --help-variable.") |
| 383 | (defvar cmake-help-variable-history nil "Variable read history.") |
| 384 | (defvar cmake-properties '() "List of available topics for --help-property.") |
| 385 | (defvar cmake-help-property-history nil "Property read history.") |
| 386 | (defvar cmake-help-complete-history nil "Complete help read history.") |
| 387 | (defvar cmake-string-to-list-symbol |
| 388 | '(("command" cmake-commands cmake-help-command-history) |
| 389 | ("module" cmake-modules cmake-help-module-history) |
| 390 | ("variable" cmake-variables cmake-help-variable-history) |
| 391 | ("property" cmake-properties cmake-help-property-history) |
| 392 | )) |
| 393 | |
| 394 | (defun cmake-get-list (listname) |
| 395 | "If the value of LISTVAR is nil, run cmake --help-LISTNAME-list |
| 396 | and store the result as a list in LISTVAR." |
| 397 | (let ((listvar (car (cdr (assoc listname cmake-string-to-list-symbol))))) |
| 398 | (if (not (symbol-value listvar)) |
| 399 | (let ((temp-buffer-name "*CMake Temporary*")) |
| 400 | (save-window-excursion |
| 401 | (cmake-command-run-help (concat "--help-" listname "-list") nil temp-buffer-name) |
| 402 | (with-current-buffer temp-buffer-name |
| 403 | ; FIXME: Ignore first line if it is "cmake version ..." from CMake < 3.0. |
| 404 | (set listvar (split-string (buffer-substring-no-properties (point-min) (point-max)) "\n" t))))) |
| 405 | (symbol-value listvar) |
| 406 | )) |
| 407 | ) |
| 408 | |
| 409 | (require 'thingatpt) |
| 410 | (defun cmake-symbol-at-point () |
| 411 | (let ((symbol (symbol-at-point))) |
| 412 | (and (not (null symbol)) |
| 413 | (symbol-name symbol)))) |
| 414 | |
| 415 | (defun cmake-help-type (type) |
| 416 | (let* ((default-entry (cmake-symbol-at-point)) |
| 417 | (history (car (cdr (cdr (assoc type cmake-string-to-list-symbol))))) |
| 418 | (input (completing-read |
| 419 | (format "CMake %s: " type) ; prompt |
| 420 | (cmake-get-list type) ; completions |
| 421 | nil ; predicate |
| 422 | t ; require-match |
| 423 | default-entry ; initial-input |
| 424 | history |
| 425 | ))) |
| 426 | (if (string= input "") |
| 427 | (error "No argument given") |
| 428 | input)) |
| 429 | ) |
| 430 | |
| 431 | ;;;###autoload |
| 432 | (defun cmake-help-command () |
| 433 | "Prints out the help message for the command the cursor is on." |
| 434 | (interactive) |
| 435 | (cmake-command-run-help "--help-command" (cmake-help-type "command") "*CMake Help*")) |
| 436 | |
| 437 | ;;;###autoload |
| 438 | (defun cmake-help-module () |
| 439 | "Prints out the help message for the module the cursor is on." |
| 440 | (interactive) |
| 441 | (cmake-command-run-help "--help-module" (cmake-help-type "module") "*CMake Help*")) |
| 442 | |
| 443 | ;;;###autoload |
| 444 | (defun cmake-help-variable () |
| 445 | "Prints out the help message for the variable the cursor is on." |
| 446 | (interactive) |
| 447 | (cmake-command-run-help "--help-variable" (cmake-help-type "variable") "*CMake Help*")) |
| 448 | |
| 449 | ;;;###autoload |
| 450 | (defun cmake-help-property () |
| 451 | "Prints out the help message for the property the cursor is on." |
| 452 | (interactive) |
| 453 | (cmake-command-run-help "--help-property" (cmake-help-type "property") "*CMake Help*")) |
| 454 | |
| 455 | ;;;###autoload |
| 456 | (defun cmake-help () |
| 457 | "Queries for any of the four available help topics and prints out the appropriate page." |
| 458 | (interactive) |
| 459 | (let* ((default-entry (cmake-symbol-at-point)) |
| 460 | (command-list (cmake-get-list "command")) |
| 461 | (variable-list (cmake-get-list "variable")) |
| 462 | (module-list (cmake-get-list "module")) |
| 463 | (property-list (cmake-get-list "property")) |
| 464 | (all-words (append command-list variable-list module-list property-list)) |
| 465 | (input (completing-read |
| 466 | "CMake command/module/variable/property: " ; prompt |
| 467 | all-words ; completions |
| 468 | nil ; predicate |
| 469 | t ; require-match |
| 470 | default-entry ; initial-input |
| 471 | 'cmake-help-complete-history |
| 472 | ))) |
| 473 | (if (string= input "") |
| 474 | (error "No argument given") |
| 475 | (if (member input command-list) |
| 476 | (cmake-command-run-help "--help-command" input "*CMake Help*") |
| 477 | (if (member input variable-list) |
| 478 | (cmake-command-run-help "--help-variable" input "*CMake Help*") |
| 479 | (if (member input module-list) |
| 480 | (cmake-command-run-help "--help-module" input "*CMake Help*") |
| 481 | (if (member input property-list) |
| 482 | (cmake-command-run-help "--help-property" input "*CMake Help*") |
| 483 | (error "Not a know help topic.") ; this really should not happen |
| 484 | )))))) |
| 485 | ) |
| 486 | |
| 487 | ;;;###autoload |
| 488 | (progn |
| 489 | (add-to-list 'auto-mode-alist '("CMakeLists\\.txt\\'" . cmake-mode)) |
| 490 | (add-to-list 'auto-mode-alist '("\\.cmake\\'" . cmake-mode))) |
| 491 | |
| 492 | ; This file provides cmake-mode. |
| 493 | (provide 'cmake-mode) |
| 494 | |
| 495 | ;;; cmake-mode.el ends here |