var katescript = { "name": "CMake", "author": "Alex Turbov ", "license": "LGPL", "revision": 2, "kate-version": "5.1", "required-syntax-style": "CMake", "indent-languages": ["CMake"] }; // kate-script-header, must be at the start of the file without comments, pure json /* This file is part of the Kate Project. SPDX-License-Identifier: LGPL-2.0-or-later */ /** * Some settings it assumes being in effect: * indent-width 4; * space-indent true; * * \todo Better to check (assert) some of that modelines... */ // required katepart js libraries require ("cmake_indenter_config.js") require ("range.js"); require ("string.js"); require ("utils.js") require ("underscore.js") triggerCharacters = "()$<{\""; //BEGIN Configuration var debugMode = false; //END Configuration /// Global var to store indentation width for current document type var gIndentWidth = 4; /// Map of CMake calls where a key is the \e end of a corresponding \e start call var END_BEGIN_CALL_PAIRS = { "endfunction": "function" , "endmacro": "macro" , "endforeach" : "foreach" , "endif" : {target: ["if", "else", "elseif"], push: ["endif"], pop: ["if"]} , "elseif" : {target: ["if", "elseif"], push: ["endif"], pop: ["if"]} , "else" : {target: ["if", "elseif"], push: ["endif"], pop: ["if"]} , "endwhile" : "while" }; /// List of CMake commands which introduce indentation decrease var CONTROL_FLOW_CALLS_TO_UNINDENT_AFTER = [ "break", "return" ]; /// List of CMake commands which introduce indentation increase var CONTROL_FLOW_CALLS_TO_INDENT_AFTER = [ "function", "macro", "foreach", "if", "else", "elseif", "while" ]; /// List of CMake calls w/o (and/or all optional) parameters var PARAMETERLESS_CALLS = [ "break", "else", "elseif", "enable_testing", "endforeach", "endif", "endmacro", "endwhile", "return" ]; /// List of CMake command options which have parameter(s) var INDENT_AFTER_OPTIONS = [ "OUTPUT" // add_custom_command , "COMMAND" // add_custom_command, add_custom_target, add_test, execute_process, if , "ARGS" // add_custom_command, try_run , "MAIN_DEPENDENCY" // add_custom_command , "DEPENDS" // add_custom_command, add_custom_target , "IMPLICIT_DEPENDS" // add_custom_command , "WORKING_DIRECTORY" // add_custom_command, add_custom_target, add_test, execute_process , "COMMENT" // add_custom_command, add_custom_target , "TARGET" // add_custom_command, build_command, get_property, if, set_property , "SOURCES" // add_custom_target, try_compile , "ALIAS" // add_executable , "OBJECT" // add_library, add_library , "NAME" // add_test , "CONFIGURATIONS" // add_test, install , "CONFIGURATION" // build_command , "PROJECT_NAME" // build_command , "RESULT" // cmake_host_system_information , "QUERY" // cmake_host_system_information , "VERSION" // cmake_minimum_required, cmake_policy , "SET" // cmake_policy , "GET" // cmake_policy, list , "NEWLINE_STYLE" // configure_file , "EXTRA_INCLUDE" // create_test_sourcelist , "FUNCTION" // create_test_sourcelist , "PROPERTY" // define_property, get_property, set_property , "BRIEF_DOCS" // define_property , "FULL_DOCS" // define_property , "TIMEOUT" // execute_process, file , "RESULT_VARIABLE" // execute_process, include , "OUTPUT_VARIABLE" // execute_process, try_compile, try_run , "ERROR_VARIABLE" // execute_process , "INPUT_FILE" // execute_process , "OUTPUT_FILE" // execute_process , "ERROR_FILE" // execute_process , "TARGETS" // export, install , "NAMESPACE" // export, install , "FILE" // export, install , "PACKAGE" // export , "WRITE" // file , "APPEND" // file, list , "READ" // file , "LIMIT" // file , "OFFSET" // file , "STRINGS" // file , "LIMIT_COUNT" // file , "LIMIT_INPUT" // file , "LIMIT_OUTPUT" // file , "LENGTH_MINIMUM" // file , "LENGTH_MAXIMUM" // file , "REGEX" // file, string , "GLOB" // file , "RELATIVE" // file , "GLOB_RECURSE" // file , "RENAME" // file, install , "REMOVE" // file , "REMOVE_RECURSE" // file , "MAKE_DIRECTORY" // file , "RELATIVE_PATH" // file , "TO_CMAKE_PATH" // file , "TO_NATIVE_PATH" // file , "DOWNLOAD" // file , "INACTIVITY_TIMEOUT" // file , "STATUS" // file , "LOG" // file , "EXPECTED_HASH" // file , "EXPECTED_MD5" // file , "TLS_VERIFY" // file , "TLS_CAINFO" // file , "UPLOAD" // file , "TIMESTAMP" // file, string , "GENERATE" // file , "INPUT" // file , "CONTENT" // file , "CONDITION" // file , "COPY" // file , "INSTALL" // file , "DESTINATION" // file, install , "FILE_PERMISSIONS" // file, install , "DIRECTORY_PERMISSIONS" // file, install , "FILES_MATCHING" // file, install , "PATTERN" // file, install , "PERMISSIONS" // file, install , "EXCLUDE" // file, install, load_cache , "NAMES" // find_file, find_library, find_package, find_path, find_program , "HINTS" // find_file, find_library, find_package, find_path, find_program , "ENV" // find_file, find_library, find_path, find_program , "PATHS" // find_file, find_library, find_package, find_path, find_program , "PATH_SUFFIXES" // find_file, find_library, find_package, find_path, find_program , "DOC" // find_file, find_library, find_path, find_program , "REQUIRED" // find_package , "COMPONENTES" // find_package , "OPTIONAL_COMPONENTES" // find_package , "CONFIGS" // find_package , "DIRECTORY" // get_directory_property, get_property, install, set_property , "DEFINITION" // get_directory_property , "PROGRAM" // get_filename_component , "PROGRAM_ARGS" // get_filename_component , "SOURCE" // get_property, set_property , "TEST" // get_property, set_property , "CACHE" // get_property, set, set_property , "NOT" // if , "AND" // if , "OR" // if , "POLICY" // if , "EXISTS" // if , "IS_NEWER_THAN" // if , "IS_DIRECTORY" // if , "IS_SYMLINK" // if , "IS_ABSOLUTE" // if , "MATCHES" // if , "LESS" // if, string , "GREATER" // if, string , "EQUAL" // if, string , "STRLESS" // if , "STRGREATER" // if , "STREQUAL" // if , "VERSION_LESS" // if , "VERSION_GREATER" // if , "VERSION_EQUAL" // if , "DEFINED" // if , "TYPE" // include_external_msproject , "GUID" // include_external_msproject , "PLATFORM" // include_external_msproject , "EXPORT" // install , "ARCHIVE" // install , "LIBRARY" // install , "RUNTIME" // install , "FRAMEWORK" // install , "BUNDLE" // install , "PRIVATE_HEADER" // install , "PUBLIC_HEADER" // install , "RESOURCE" // install , "COMPONENT" // install , "FILES" // install , "PROGRAMS" // install , "SCRIPT" // install , "CODE" // install , "LENGTH" // list, string , "FIND" // list, string , "INSERT" // list , "REMOVE_ITEM" // list , "REMOVE_AT" // list , "REMOVE_DUPLICATES" // list , "REVERSE" // list , "SORT" // list , "READ_WITH_PREFIX" // load_cache , "INCLUDE_INTERNALS" // load_cache , "EXPR" // math , "UNIX_COMMAND" // separate_arguments , "WINDOWS_COMMAND" // separate_arguments // set_directory_properties, set_source_files_properties, set_target_properties, set_tests_properties , "PROPERTIES" , "REGULAR_EXPRESSION" // source_group , "FILES" // source_group , "MATCH" // string , "MATCHALL" // string , "REPLACE" // string , "COMPARE" // string , "NOTEQUAL" // string , "ASCII" // string , "CONFIGURE" // string , "TOUPPER" // string , "TOLOWER" // string , "SUBSTRING" // string , "STRIP" // string , "RANDOM" // string , "ALPHABET" // string , "RANDOM_SEED" // string , "MAKE_C_IDENTIFIER" // string , "INTERFACE" // target_compile_definitions, target_compile_options, target_include_directories , "PUBLIC" // target_compile_definitions, target_compile_options, target_include_directories , "PRIVATE" // target_compile_definitions, target_compile_options, target_include_directories , "LINK_PRIVATE" // target_link_libraries , "LINK_PUBLIC" // target_link_libraries , "CMAKE_FLAGS" // try_compile, try_run , "COMPILE_DEFINITIONS" // try_compile, try_run , "LINK_LIBRARIES" // try_compile , "COPY_FILE" // try_compile , "COPY_FILE_ERROR" // try_compile , "COMPILE_OUTPUT_VARIABLE" // try_run , "RUN_OUTPUT_VARIABLE" // try_run ]; /** * \brief Handle \c ENTER between parenthesis */ function tryParenthesisSplit_ch(cursor) { var result = -1; if (isStringOrComment(cursor.line - 1, cursor.column)) return result; // Do nothing for comments and strings // Is ENTER was pressed right after '('? if (document.lastChar(cursor.line - 1) == '(') { // Yep, lets get align of the previous line var prev_line_indent = document.firstColumn(cursor.line - 1); result = prev_line_indent + gIndentWidth; // Indent a closing ')' if ENTER was pressed between '(|)' if (document.firstChar(cursor.line) == ')') { document.insertText( cursor.line , cursor.column , "\n" + String().fill(' ', prev_line_indent + gIndentWidth / 2) ); view.setCursorPosition(cursor.line, cursor.column); } } if (result != -1) { dbg("tryParenthesisSplit_ch: result =", result); } return result; } /** * \brief Indent after some control flow CMake calls */ function tryIndentAfterControlFlowCalls_ch(cursor) { var result = -1; if (isStringOrComment(cursor.line - 1, cursor.column)) return result; // Do nothing for comments and strings var prev_line_indent = document.firstColumn(cursor.line - 1); var first_prev_line_word = document.wordAt(cursor.line - 1, prev_line_indent).toLowerCase(); dbg("tryControlFlowCalls_ch: first_prev_line_word =", first_prev_line_word); if (CONTROL_FLOW_CALLS_TO_INDENT_AFTER.indexOf(first_prev_line_word) != -1) { result = prev_line_indent + gIndentWidth; } else if (CONTROL_FLOW_CALLS_TO_UNINDENT_AFTER.indexOf(first_prev_line_word) != -1) { result = prev_line_indent - gIndentWidth; } if (result != -1) { dbg("tryControlFlowCalls_ch: result =", result); } return result; } /** * \brief Unindent after dandling ')' */ function tryAfterClosingParensis_ch(cursor) { var result = -1; if (isStringOrComment(cursor.line - 1, cursor.column)) return result; // Do nothing for comments and strings // Check if dandling ')' present on a previous line var prev_line = document.prevNonEmptyLine(cursor.line - 1); dbg("tryAfterClosingParensis_ch: prev_line =", prev_line); var first_column = document.firstColumn(prev_line); if (document.charAt(prev_line, first_column) == ')') result = first_column - (gIndentWidth / 2); if (result != -1) { dbg("tryAfterClosingParensis_ch: result =", result); } return result; } /** * \brief Indent after parameterless command options * * It is common way to format options w/ parameters like this: * \code * install( * DIRECTORY ${name} * DESTINATION ${DATA_INSTALL_DIR}/kate/pate * FILES_MATCHING * PATTERN "*.py" * PATTERN "*.ui" * PATTERN "*_ui.rc" * PATTERN "__pycache__*" EXCLUDE * ) * \endcode * * I.e. do indent for long parameter lists. * This function recognize parameterless options (and do not indent after them), * for everything else it adds an extra indentation... */ function tryIndentCommandOptions_ch(cursor) { var result = -1; if (isStringOrComment(cursor.line - 1, cursor.column)) return result; // Do nothing for comments and strings // Get last word from a previous line var last_word = document.wordAt(cursor.line - 1, document.lastColumn(cursor.line - 1)); // ATTENTION Kate will return the last word "TARGET" for text like this: "TARGET)", // which is not what we've wanted... so before continue lets make sure that we've // got that we wanted... if (last_word[last_word.length - 1] != document.lastChar(cursor.line - 1)) return result; dbg("tryIndentCommandOptions_ch: last_word =", last_word); result = document.firstColumn(cursor.line - 1) + (INDENT_AFTER_OPTIONS.indexOf(last_word) != -1 ? gIndentWidth : 0) ; if (result != -1) { dbg("tryIndentCommandOptions_ch: result =", result); } return result; } /** * \brief Handle \c ENTER key */ function caretPressed(cursor) { var result = -1; var line = cursor.line; // Dunno what to do if previous line isn't available if (line - 1 < 0) return result; // Nothing (dunno) to do if no previous line... // Register all indent functions var handlers = [ tryParenthesisSplit_ch // Handle ENTER between parenthesis , tryIndentAfterControlFlowCalls_ch // Indent after some control flow CMake calls , tryAfterClosingParensis_ch // Unindent after dandling ')' , tryIndentCommandOptions_ch // Indent after parameterless command options ]; // Apply all functions until result gets changed for ( var i = 0 ; i < handlers.length && result == -1 ; result = handlers[i++](cursor) ); return result; } /** * \brief Try to unindent current call when ( has pressed */ function tryUnindentCall(cursor) { var result = -2; if (isStringOrComment(cursor.line, cursor.column)) return result; // Do nothing for comments and strings // Check the word before current cursor position var call_name = document.wordAt(cursor.line, cursor.column - 1).toLowerCase(); dbg("tryUnindentCall: call_name =", call_name); var end_calls = _.keys(END_BEGIN_CALL_PAIRS); var stack_level = 0; if (_.indexOf(end_calls, call_name) != -1) { var lookup_call_name = END_BEGIN_CALL_PAIRS[call_name]; dbg("tryUnindentCall: typeof lookup_call_name =", typeof lookup_call_name); var matcher; if (typeof lookup_call_name === "string") { matcher = function(word) { return word == lookup_call_name; }; } else if (typeof lookup_call_name === "object") { matcher = function(word) { // dbg("tryUnindentCall: stack_level =", stack_level, ", word =", word); if (_.indexOf(lookup_call_name.push, word) != -1) { stack_level += 1; return false; } if (stack_level != 0) { if (_.indexOf(lookup_call_name.pop, word) != -1) stack_level -= 1; return false; } return _.indexOf(lookup_call_name.target, word) != -1; }; } else { /// \todo Is there any kind of \c assert() in JavaScript at all? dbg("tryUnindentCall: Unknown type of END_BEGIN_CALL_PAIRS value! Code review required!"); return result; } // Ok, lets find the corresponding start call towards document's start... var found_line = -1; for ( var line = document.prevNonEmptyLine(cursor.line - 1) ; 0 <= line && found_line == -1 ; line = document.prevNonEmptyLine(line - 1) ) { var first_word = document.wordAt(line, document.firstColumn(line)).toLowerCase(); // dbg("tryUnindentCall: line =", line, ", word =", first_word); if (matcher(first_word)) { dbg("tryUnindentCall: Found ", first_word); found_line = line; } } if (found_line != -1) { result = document.firstColumn(found_line); // Add closing parenthesis if none yet // addCharOrJumpOverIt(cursor.line, cursor.column, ')'); } else { dbg("tryUnindentCall: Not Found!"); } } if (result != -2) { dbg("tryUnindentCall: result =", result); } return result; } /** * Align closing parenthesis if it is a first character on a line */ function tryAlignCloseParenthesis(cursor) { var result = -2; if (isStringOrComment(cursor.line, cursor.column)) return result; // Do nothing for comments and strings if (justEnteredCharIsFirstOnLine(cursor.line, cursor.column, ')')) { // Try to find corresponding open parenthesis var open = document.anchor(cursor.line, cursor.column - 1, '('); dbg("tryAlignCloseParenthesis: Found open brace at", open); if (open.isValid()) result = document.firstColumn(open.line) + (gIndentWidth / 2); } if (result != -2) { dbg("tryAlignCloseParenthesis: result =", result); } return result; } /** * \brief Move cursor out of '()' if no parameters required for a given call */ function tryJumpOutOfParenthesis(cursor) { if (isStringOrComment(cursor.line, cursor.column)) return; // Do nothing for comments and strings var first_word = document.wordAt(cursor.line, document.firstColumn(cursor.line)).toLowerCase(); dbg("tryJumpOutOfParenthesis: @"+cursor.line+","+cursor.column+", char="+document.charAt(cursor)); if (PARAMETERLESS_CALLS.indexOf(first_word) != -1) addCharOrJumpOverIt(cursor.line, cursor.column, ')') } /** * \brief Append '{}' after \c '$' */ function insertVariableExpansion(cursor) { if (!cmi_cfg_vgShortcut || isComment(cursor.line, cursor.column)) return; // Do nothing for comments var next_ch = document.charAt(cursor); dbg("insertVariableExpansion: next_ch ='"+next_ch+"'"); if (!next_ch.match(/[A-Za-z_]/)) { if (next_ch != '{') document.insertText(cursor, "{}"); view.setCursorPosition(cursor.line, cursor.column + 1); } } /** * \brief Handle '$' character * * Here is possble few transformations: if \c '<' pressed after \c '$', * transform '${<}' to '$<>' to be ready for * generator expression and vise versa. */ function tryVariableOrGeneratorExpression(cursor, ch) { if (!cmi_cfg_vgShortcut || isComment(cursor.line, cursor.column)) return; // Do nothing for comments and strings var tail = document.text( cursor.line , document.firstColumn(cursor.line) , cursor.line , cursor.column ); var next_ch = document.charAt(cursor); dbg("tryVariableOrGeneratorExpression: tail ='"+tail+"'"); var fix_text = function(text) { document.removeText(cursor.line, cursor.column - 2, cursor.line, cursor.column + 1); document.insertText(cursor.line, cursor.column - 2, text); view.setCursorPosition(cursor.line, cursor.column - 1); }; if (tail.endsWith("${<") && next_ch == '}') { fix_text("<>"); // Transform '${}' -> '$<>' } else if (tail.endsWith("$<{") && next_ch == '>') { fix_text("{}"); // Transform '$<>' -> '${}' } else if (tail.endsWith("${{") && next_ch == '}') { // Transform '${{|}' --> '${|}' document.removeText(cursor.line, cursor.column - 1, cursor.line, cursor.column); } else if (tail.endsWith("$<<") && next_ch == '>') { // Transform '$<<|>' --> '$<|>' document.removeText(cursor.line, cursor.column - 1, cursor.line, cursor.column); } } /** * \brief Open/close string literal * * \attention Autobracket extension w/ add quote char doesn't work * for CMake files (file a BUG?)... */ function tryString(cursor) { if (isComment(cursor.line, cursor.column)) return; // Do nothing for comments // NOTE If cursor has a string attribute, then it was an open quote // character just entered... // ATTENTION If cursor positioned at the end of a line, // isString() returns 'false' for some unknown reason... // so check the attribute for the just entered quote symbol... if (isString(cursor.line, cursor.column - 1)) { // Check if next char is not a quote already // and/or maybe some punctualtion... Particularly // ')' remains after some function call -- i.e. smth like // message(STATUS "|) // or just entered quot char is a first on a line and // no other chars are here... var ch = document.charAt(cursor); if ((ch != '"' && (ch == ')' || ch == ' ')) || (justEnteredCharIsFirstOnLine(cursor.line, cursor.column, '"') && document.lineLength(cursor.line) == cursor.column ) ) { document.insertText(cursor, '"') view.setCursorPosition(cursor); } } // Do nothing for closing quote char } /** * \brief Process one character * * NOTE Cursor positioned right after just entered character and has \c +1 in column. */ function processChar(line, ch) { var result = -2; // By default, do nothing... var cursor = view.cursorPosition(); if (!cursor) return result; document.editBegin(); // Check if char under cursor is the same as just entered, // and if so, remove it... to make it behave like "overwrite" mode if (ch != ' ' && document.charAt(cursor) == ch) document.removeText(line, cursor.column, line, cursor.column + 1); switch (ch) { case '\n': result = caretPressed(cursor); break; case '(': result = tryUnindentCall(cursor); tryJumpOutOfParenthesis(cursor); break; case ')': result = tryAlignCloseParenthesis(cursor); break; case '$': insertVariableExpansion(cursor); break; case '{': case '<': tryVariableOrGeneratorExpression(cursor, ch); break; case '"': tryString(cursor); break; default: break; // Nothing to do... } document.editEnd(); return result; } /** * Try to align a given line * \todo More actions */ function indentLine(line) { dbg(">> Going to indent line ", line); var cursor = new Cursor(line, document.firstColumn(line) + 1); var result = tryUnindentCall(cursor); if (result == -2) result = tryAlignCloseParenthesis(cursor); cursor = new Cursor(line, document.firstColumn(line)); if (result == -2) result = caretPressed(cursor); dbg("indentLine: result =", result); if (result == -2) // Still dunno what to do? result = -1; // ... just align according a previous non empty line return result; } /** * \brief Process a newline or one of \c triggerCharacters character. * * This function is called whenever the user hits \c ENTER key. * * It gets three arguments: \c line, \c indentwidth in spaces and typed character * * Called for each newline (ch == \n) and all characters specified in * the global variable \c triggerCharacters. When calling \e Tools->Align * the variable \c ch is empty, i.e. ch == ''. */ function indent(line, indentWidth, ch) { // NOTE Update some global variables gIndentWidth = indentWidth; dbg("indentWidth: " + indentWidth); dbg(" gMode: " + document.highlightingModeAt(view.cursorPosition())); dbg(" gAttr: " + document.attributeName(view.cursorPosition())); dbg(" line: " + line); dbg(" ch: '" + ch + "'"); if (ch != "") return processChar(line, ch); return indentLine(line); } // kate: space-indent on; indent-width 4; replace-tabs on; /** * \todo Move string/comment checks to \c caretPressed() instead of particular function. */