diff --git a/assets/js/plugin-check-admin.js b/assets/js/plugin-check-admin.js index 086eef5c4..45b35d03d 100644 --- a/assets/js/plugin-check-admin.js +++ b/assets/js/plugin-check-admin.js @@ -378,36 +378,63 @@ Date.now().toString( 36 ) + Math.random().toString( 36 ).substr( 2 ); + // Check if any errors or warnings have links. + const hasLinks = + hasLinksInResults( errors ) || hasLinksInResults( warnings ); + // Render the file table. resultsContainer.innerHTML += renderTemplate( 'plugin-check-results-table', - { file, index } + { file, index, hasLinks } ); const resultsTable = document.getElementById( 'plugin-check__results-body-' + index ); // Render results to the table. - renderResultRows( 'ERROR', errors, resultsTable ); - renderResultRows( 'WARNING', warnings, resultsTable ); + renderResultRows( 'ERROR', errors, resultsTable, hasLinks ); + renderResultRows( 'WARNING', warnings, resultsTable, hasLinks ); } /** - * Renders a result row onto the file table. + * Checks if there are any links in the results object. * * @since n.e.x.t * - * @param {string} type The result type. Either ERROR or WARNING. * @param {Object} results The results object. - * @param {Object} table The HTML table to append a result row to. + * @return {boolean} True if there are links, false otherwise. + */ + function hasLinksInResults( results ) { + for ( const line in results ) { + for ( const column in results[ line ] ) { + for ( let i = 0; i < results[ line ][ column ].length; i++ ) { + if ( results[ line ][ column ][ i ].link ) { + return true; + } + } + } + } + return false; + } + + /** + * Renders a result row onto the file table. + * + * @since n.e.x.t + * + * @param {string} type The result type. Either ERROR or WARNING. + * @param {Object} results The results object. + * @param {Object} table The HTML table to append a result row to. + * @param {boolean} hasLinks Whether any result has links. */ - function renderResultRows( type, results, table ) { + function renderResultRows( type, results, table, hasLinks ) { // Loop over each result by the line, column and messages. for ( const line in results ) { for ( const column in results[ line ] ) { for ( let i = 0; i < results[ line ][ column ].length; i++ ) { const message = results[ line ][ column ][ i ].message; const code = results[ line ][ column ][ i ].code; + const link = results[ line ][ column ][ i ].link; table.innerHTML += renderTemplate( 'plugin-check-results-row', @@ -417,6 +444,8 @@ type, message, code, + link, + hasLinks, } ); } diff --git a/includes/Admin/Admin_Page.php b/includes/Admin/Admin_Page.php index 4e6b61c6b..ba790c737 100644 --- a/includes/Admin/Admin_Page.php +++ b/includes/Admin/Admin_Page.php @@ -51,6 +51,7 @@ public function __construct( Admin_AJAX $admin_ajax ) { public function add_hooks() { add_action( 'admin_menu', array( $this, 'add_and_initialize_page' ) ); add_filter( 'plugin_action_links', array( $this, 'filter_plugin_action_links' ), 10, 4 ); + add_action( 'admin_enqueue_scripts', array( $this, 'add_jump_to_line_code_editor' ) ); $this->admin_ajax->add_hooks(); } @@ -123,6 +124,41 @@ public function enqueue_scripts() { ); } + /** + * Enqueue a script in the WordPress admin on plugin-editor.php. + * + * @since n.e.x.t + * + * @param string $hook_suffix The current admin page. + */ + public function add_jump_to_line_code_editor( $hook_suffix ) { + if ( 'plugin-editor.php' !== $hook_suffix ) { + return; + } + + $line = (int) ( $_GET['line'] ?? 0 ); + if ( ! $line ) { + return; + } + + wp_add_inline_script( + 'wp-theme-plugin-editor', + sprintf( + ' + ( + ( originalInitCodeEditor ) => { + wp.themePluginEditor.initCodeEditor = function() { + originalInitCodeEditor.apply( this, arguments ); + this.instance.codemirror.doc.setCursor( %d - 1 ); + }; + } + )( wp.themePluginEditor.initCodeEditor ); + ', + wp_json_encode( $line ) + ) + ); + } + /** * Returns the list of plugins. * diff --git a/includes/Checker/Check_Result.php b/includes/Checker/Check_Result.php index c06a734e7..4750c09f2 100644 --- a/includes/Checker/Check_Result.php +++ b/includes/Checker/Check_Result.php @@ -90,6 +90,7 @@ public function plugin() { * @type string $file The file in which the message occurred. Default empty string (unknown file). * @type int $line The line on which the message occurred. Default 0 (unknown line). * @type int $column The column on which the message occurred. Default 0 (unknown column). + * @type string $link View in code editor link. Default empty string. * } */ public function add_message( $error, $message, $args = array() ) { @@ -98,6 +99,7 @@ public function add_message( $error, $message, $args = array() ) { 'file' => '', 'line' => 0, 'column' => 0, + 'link' => '', ); $data = array_merge( diff --git a/includes/Traits/Amend_Check_Result.php b/includes/Traits/Amend_Check_Result.php index a9e76311c..b13bb3c4a 100644 --- a/includes/Traits/Amend_Check_Result.php +++ b/includes/Traits/Amend_Check_Result.php @@ -16,6 +16,8 @@ */ trait Amend_Check_Result { + use File_Editor_URL; + /** * Amends the given result with a message for the specified file, including error information. * @@ -38,6 +40,7 @@ protected function add_result_message_for_file( Check_Result $result, $error, $m 'file' => str_replace( $result->plugin()->path(), '', $file ), 'line' => $line, 'column' => $column, + 'link' => $this->get_file_editor_url( $result, $file, $line ), ) ); } diff --git a/includes/Traits/File_Editor_URL.php b/includes/Traits/File_Editor_URL.php new file mode 100644 index 000000000..758adadd8 --- /dev/null +++ b/includes/Traits/File_Editor_URL.php @@ -0,0 +1,116 @@ +plugin()->path( '/' ); + $plugin_slug = basename( $plugin_path ); + $filename = str_replace( $plugin_path, '', $filename ); + /** + * Filters the template for the URL for linking to an external editor to open a file for editing. + * + * Users of IDEs that support opening files in via web protocols can use this filter to override + * the edit link to result in their editor opening rather than the plugin editor. + * + * The initial filtered value is null, requiring extension plugins to supply the URL template + * string themselves. If no template string is provided, links to the plugin editors will + * be provided if available. For example, for an extension plugin to cause file edit links to + * open in an IDE, the following filters can be used: + * + * # PhpStorm + * add_filter( 'wp_plugin_check_validation_error_source_file_editor_url_template', function () { + * return 'phpstorm://open?file={{file}}&line={{line}}'; + * } ); + * + * # VS Code + * add_filter( 'wp_plugin_check_validation_error_source_file_editor_url_template', function () { + * return 'vscode://file/{{file}}:{{line}}'; + * } ); + * + * For a template to be considered, the string '{{file}}' must be present in the filtered value. + * + * @since n.e.x.t + * + * @param string|null $editor_url_template Editor URL template. default null. + */ + $editor_url_template = apply_filters( 'wp_plugin_check_validation_error_source_file_editor_url_template', null ); + + // Supply the file path to the editor template. + if ( is_string( $editor_url_template ) && str_contains( $editor_url_template, '{{file}}' ) ) { + $file_path = WP_PLUGIN_DIR . '/' . $plugin_slug; + if ( $plugin_slug !== $filename ) { + $file_path .= '/' . $filename; + } + + if ( file_exists( $file_path ) ) { + /** + * Filters the file path to be opened in an external editor for a given PHPCS error source. + * + * This is useful to map the file path from inside of a Docker container or VM to the host machine. + * + * @since n.e.x.t + * + * @param string|null $editor_url_template Editor URL template. + * @param array $source Source information. + */ + $file_path = apply_filters( 'wp_plugin_check_validation_error_source_file_path', $file_path, array( $plugin_slug, $filename, $line ) ); + if ( $file_path ) { + $edit_url = str_replace( + array( + '{{file}}', + '{{line}}', + ), + array( + rawurlencode( $file_path ), + $line, + ), + $editor_url_template + ); + } + } + } + + // Fall back to using the plugin editor if no external editor is offered. + if ( ! $edit_url && current_user_can( 'edit_plugins' ) ) { + $query_args = array( + 'plugin' => rawurlencode( $result->plugin()->basename() ), + 'file' => rawurlencode( $plugin_slug . '/' . $filename ), + ); + if ( $line ) { + $query_args['line'] = $line; + } + return add_query_arg( + $query_args, + admin_url( 'plugin-editor.php' ) + ); + } + return $edit_url; + } +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 1f85efe98..b8b9a4db3 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -20,3 +20,4 @@ parameters: paths: - includes/Checker/Checks/Abstract_File_Check.php - includes/Traits/Find_Readme.php + - includes/Traits/File_Editor_URL.php diff --git a/templates/results-row.php b/templates/results-row.php index 8a3b6d6b1..4d0733bcb 100644 --- a/templates/results-row.php +++ b/templates/results-row.php @@ -14,5 +14,15 @@ {{data.message}} + <# if ( data.hasLinks ) { #> + + <# if ( data.link ) { #> + + + + + <# } #> + + <# } #> diff --git a/templates/results-table.php b/templates/results-table.php index 7af4860af..2c10949b2 100644 --- a/templates/results-table.php +++ b/templates/results-table.php @@ -17,6 +17,11 @@ + <# if ( data.hasLinks ) { #> + + + + <# } #> diff --git a/tests/phpunit/Checker/Check_Result_Tests.php b/tests/phpunit/Checker/Check_Result_Tests.php index a97369953..d97d7e093 100644 --- a/tests/phpunit/Checker/Check_Result_Tests.php +++ b/tests/phpunit/Checker/Check_Result_Tests.php @@ -59,6 +59,7 @@ public function test_add_message_with_warning() { $expected = array( 'message' => 'Warning message', 'code' => 'test_warning', + 'link' => '', ); $this->assertEquals( $expected, $warnings['test-plugin.php'][12][40][0] ); @@ -92,6 +93,7 @@ public function test_add_message_with_error() { $expected = array( 'message' => 'Error message', 'code' => 'test_error', + 'link' => '', ); $this->assertEquals( $expected, $errors['test-plugin.php'][22][30][0] ); @@ -122,6 +124,7 @@ public function test_get_errors_with_errors() { $expected = array( 'message' => 'Error message', 'code' => 'test_error', + 'link' => '', ); $this->assertEquals( $expected, $errors['test-plugin.php'][22][30][0] ); @@ -152,6 +155,7 @@ public function test_get_warnings_with_warnings() { $expected = array( 'message' => 'Warning message', 'code' => 'test_warning', + 'link' => '', ); $this->assertEquals( $expected, $warnings['test-plugin.php'][22][30][0] );