From 793cc00b1a2b88cc4d4c0b47d269e5ef75e71305 Mon Sep 17 00:00:00 2001 From: hafeoz <95505675+hafeoz@users.noreply.github.com> Date: Tue, 11 Jan 2022 21:55:50 +0000 Subject: [PATCH 1/8] Use cargo json diagnostics --- ui/Cargo.lock | 41 +++++++++++++++++++ ui/Cargo.toml | 1 + ui/frontend/actions.ts | 8 ++-- ui/frontend/highlighting.ts | 19 ++++++--- ui/frontend/index.tsx | 4 +- ui/frontend/reducers/code.ts | 24 ++++++++++- ui/src/sandbox.rs | 77 ++++++++++++++++++++++++++++++++++-- 7 files changed, 157 insertions(+), 17 deletions(-) diff --git a/ui/Cargo.lock b/ui/Cargo.lock index 6aa9e0671..1ef8e2db6 100644 --- a/ui/Cargo.lock +++ b/ui/Cargo.lock @@ -140,6 +140,37 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "camino" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d74260d9bf6944e2208aa46841b4b8f0d7ffc0849a06837b2f510337f86b2b" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba2ae6de944143141f6155a473a6b02f66c7c3f9f47316f802f80204ebfe6e12" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + [[package]] name = "cc" version = "1.0.72" @@ -1417,6 +1448,15 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" +dependencies = [ + "serde", +] + [[package]] name = "sequence_trie" version = "0.3.6" @@ -1796,6 +1836,7 @@ name = "ui" version = "0.1.0" dependencies = [ "bodyparser", + "cargo_metadata", "corsware", "dotenv", "env_logger", diff --git a/ui/Cargo.toml b/ui/Cargo.toml index 5083a38cc..9b7826575 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -11,6 +11,7 @@ fork-bomb-prevention = [] [dependencies] bodyparser = "0.8.0" corsware = "0.2.0" +cargo_metadata = "0.14.1" dotenv = "0.15.0" env_logger = "0.9.0" iron = "0.6.0" diff --git a/ui/frontend/actions.ts b/ui/frontend/actions.ts index 0cc7b3346..e26455e0e 100644 --- a/ui/frontend/actions.ts +++ b/ui/frontend/actions.ts @@ -96,7 +96,7 @@ export enum ActionType { CompileWasmFailed = 'COMPILE_WASM_FAILED', EditCode = 'EDIT_CODE', AddMainFunction = 'ADD_MAIN_FUNCTION', - AddImport = 'ADD_IMPORT', + ApplySuggestion = 'ADD_SUGGESTION', EnableFeatureGate = 'ENABLE_FEATURE_GATE', GotoPosition = 'GOTO_POSITION', SelectText = 'SELECT_TEXT', @@ -460,8 +460,8 @@ export const editCode = (code: string) => export const addMainFunction = () => createAction(ActionType.AddMainFunction); -export const addImport = (code: string) => - createAction(ActionType.AddImport, { code }); +export const applySuggestion = (startline: number, startcol: number, endline: number, endcol: number, suggestion: string) => + createAction(ActionType.ApplySuggestion, { startline, startcol, endline, endcol, suggestion }); export const enableFeatureGate = (featureGate: string) => createAction(ActionType.EnableFeatureGate, { featureGate }); @@ -841,7 +841,7 @@ export type Action = | ReturnType | ReturnType | ReturnType - | ReturnType + | ReturnType | ReturnType | ReturnType | ReturnType diff --git a/ui/frontend/highlighting.ts b/ui/frontend/highlighting.ts index 38847b421..9a7da4883 100644 --- a/ui/frontend/highlighting.ts +++ b/ui/frontend/highlighting.ts @@ -6,7 +6,7 @@ export function configureRustErrors({ getChannel, gotoPosition, selectText, - addImport, + applySuggestion, reExecuteWithBacktrace, }) { Prism.languages.rust_errors = { @@ -30,9 +30,9 @@ export function configureRustErrors({ }, 'error-location': /-->\s+(\/playground\/)?src\/.*\n/, 'import-suggestion-outer': { - pattern: /\|\s+use\s+([^;]+);/, + pattern: /\[\[Line\s\d+\sCol\s\d+\s-\sLine\s\d+\sCol\s\d+:\s[.\s\S]+?\]\]/, inside: { - 'import-suggestion': /use\s+.*/, + 'import-suggestion': /\[\[Line\s\d+\sCol\s\d+\s-\sLine\s\d+\sCol\s\d+:\s[.\s\S]+?\]\]/, }, }, 'rust-errors-help': { @@ -87,9 +87,16 @@ export function configureRustErrors({ env.attributes['data-col'] = col; } if (env.type === 'import-suggestion') { + const errorMatch = /\[\[Line\s(\d+)\sCol\s(\d+)\s-\sLine\s(\d+)\sCol\s(\d+):\s([.\s\S]+?)\]\]/.exec(env.content); + const [_, startLine, startCol, endLine, endCol, importSuggestion] = errorMatch; env.tag = 'a'; env.attributes.href = '#'; - env.attributes['data-suggestion'] = env.content; + env.attributes['data-startline'] = startLine; + env.attributes['data-startcol'] = startCol; + env.attributes['data-endline'] = endLine; + env.attributes['data-endcol'] = endCol; + env.attributes['data-suggestion'] = importSuggestion; + env.content = 'Apply \"' + importSuggestion.trim() + '\"\n'; } if (env.type === 'feature-gate') { const [_, featureGate] = /feature\((.*?)\)/.exec(env.content); @@ -134,10 +141,10 @@ export function configureRustErrors({ const importSuggestions = env.element.querySelectorAll('.import-suggestion'); Array.from(importSuggestions).forEach((link: HTMLAnchorElement) => { - const { suggestion } = link.dataset; + const { startline, startcol, endline, endcol, suggestion } = link.dataset; link.onclick = (e) => { e.preventDefault(); - addImport(suggestion + '\n'); + applySuggestion(startline, startcol, endline, endcol, suggestion); }; }); diff --git a/ui/frontend/index.tsx b/ui/frontend/index.tsx index 12ff2be9f..b6566dea2 100644 --- a/ui/frontend/index.tsx +++ b/ui/frontend/index.tsx @@ -18,7 +18,7 @@ import { enableFeatureGate, gotoPosition, selectText, - addImport, + applySuggestion, performCratesLoad, performVersionsLoad, reExecuteWithBacktrace, @@ -61,7 +61,7 @@ configureRustErrors({ enableFeatureGate: featureGate => store.dispatch(enableFeatureGate(featureGate)), gotoPosition: (line, col) => store.dispatch(gotoPosition(line, col)), selectText: (start, end) => store.dispatch(selectText(start, end)), - addImport: (code) => store.dispatch(addImport(code)), + applySuggestion: (startline, startcol, endline, endcol, suggestion) => store.dispatch(applySuggestion(startline, startcol, endline, endcol, suggestion)), reExecuteWithBacktrace: () => store.dispatch(reExecuteWithBacktrace()), getChannel: () => store.getState().configuration.channel, }); diff --git a/ui/frontend/reducers/code.ts b/ui/frontend/reducers/code.ts index b64bacb39..e216dcece 100644 --- a/ui/frontend/reducers/code.ts +++ b/ui/frontend/reducers/code.ts @@ -19,8 +19,28 @@ export default function code(state = DEFAULT, action: Action): State { case ActionType.AddMainFunction: return `${state}\n\n${DEFAULT}`; - case ActionType.AddImport: - return action.code + state; + case ActionType.ApplySuggestion: + let state_lines = state.split("\n"); + let startline = action.startline - 1; + let endline = action.endline - 1; + let startcol = action.startcol - 1; + let endcol = action.endcol - 1; + if (startline == endline) { + state_lines[startline] = state_lines[startline].substring(0, startcol) + state_lines[startline].substring(endcol); + } else { + if (state_lines.length > startline) { + state_lines[startline] = state_lines[startline].substring(0, startcol); + } + if (state_lines.length > endline) { + state_lines[endline] = state_lines[endline].substring(endcol); + } + if (endline - startline > 1) { + state_lines.splice(startline + 1, endline - startline - 1); + } + } + state_lines[startline] = state_lines[startline].substring(0, startcol) + action.suggestion + state_lines[startline].substring(startcol); + state = state_lines.join('\n'); + return state; case ActionType.EnableFeatureGate: return `#![feature(${action.featureGate})]\n${state}`; diff --git a/ui/src/sandbox.rs b/ui/src/sandbox.rs index 6f9431e49..48d0da064 100644 --- a/ui/src/sandbox.rs +++ b/ui/src/sandbox.rs @@ -78,6 +78,8 @@ pub enum Error { UnableToReadOutput { source: io::Error }, #[snafu(display("Unable to read crate information: {}", source))] UnableToParseCrateInformation { source: ::serde_json::Error }, + #[snafu(display("Unable to parse cargo output: {}", source))] + UnableToParseCargoOutput { source: io::Error }, #[snafu(display("Output was not valid UTF-8: {}", source))] OutputNotUtf8 { source: string::FromUtf8Error }, #[snafu(display("Output was missing"))] @@ -146,8 +148,9 @@ impl Sandbox { .map(|entry| entry.path()) .find(|path| path.extension() == Some(req.target.extension())); - let stdout = vec_to_str(output.stdout)?; + let (stdout, stderr_tail) = parse_json_output(output.stdout)?; let mut stderr = vec_to_str(output.stderr)?; + stderr.push_str(&stderr_tail); let mut code = match file { Some(file) => read(&file)?.unwrap_or_else(String::new), @@ -193,10 +196,14 @@ impl Sandbox { let output = run_command_with_timeout(command)?; + let (stdout, stderr_tail) = parse_json_output(output.stdout)?; + let mut stderr = vec_to_str(output.stderr)?; + stderr.push_str(&stderr_tail); + Ok(ExecuteResponse { success: output.status.success(), - stdout: vec_to_str(output.stdout)?, - stderr: vec_to_str(output.stderr)?, + stdout, + stderr, }) } @@ -568,9 +575,73 @@ fn build_execution_command( } } + cmd.push("--message-format=json"); + cmd } +fn parse_json_output(output: Vec) -> Result<(String, String)> { + let mut composed_stderr_string = String::new(); + let mut composed_stdout_string = String::new(); + + let mut metadata_stream = cargo_metadata::Message::parse_stream(&output[..]); + + while let Some(msg) = metadata_stream.next() { + + let message = msg.context(UnableToParseCargoOutputSnafu)?; + + match message { + cargo_metadata::Message::TextLine(line) => { + composed_stdout_string.push_str(&(line + "\n")) + } + + cargo_metadata::Message::CompilerMessage(cargo_metadata::CompilerMessage { + message, + .. + }) => { + composed_stderr_string.push_str(&parse_diagnostic(message)); + } + + _ => {} + } + } + + Ok((composed_stdout_string, composed_stderr_string)) +} + +fn parse_diagnostic(diagnostic: cargo_metadata::diagnostic::Diagnostic) -> String { + let mut diagnostic_string = String::new(); + + if let Some(rendered_msg) = diagnostic.rendered { + diagnostic_string.push_str(&rendered_msg); + } else { + diagnostic_string.push_str(&diagnostic.message); + } + + for span in diagnostic.spans { + if span.file_name != "src/lib.rs" && span.file_name != "src/main.rs" { + continue; + } + + let label = if let Some(label) = span.suggested_replacement { + label + } else { + continue; + }; + + diagnostic_string.push_str(&format!( + "\n[[Line {} Col {} - Line {} Col {}: {}]]", + span.line_start, span.column_start, span.line_end, span.column_end, label + )); + } + + for children in diagnostic.children { + diagnostic_string.push_str(&parse_diagnostic(children)); + } + + diagnostic_string +} + fn set_execution_environment( cmd: &mut Command, target: Option, From 6644f8226c6812a59ddc0a0cc37b078c0fd2d742 Mon Sep 17 00:00:00 2001 From: hafeoz <95505675+hafeoz@users.noreply.github.com> Date: Tue, 11 Jan 2022 22:06:42 +0000 Subject: [PATCH 2/8] Make sure only build and run uses json output --- ui/src/sandbox.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ui/src/sandbox.rs b/ui/src/sandbox.rs index 48d0da064..584814325 100644 --- a/ui/src/sandbox.rs +++ b/ui/src/sandbox.rs @@ -533,8 +533,14 @@ fn build_execution_command( (Some(Wasm), _, _) => cmd.push("wasm"), (Some(_), _, _) => cmd.push("rustc"), (_, _, true) => cmd.push("test"), - (_, Library(_), _) => cmd.push("build"), - (_, _, _) => cmd.push("run"), + (_, Library(_), _) => { + cmd.push("build"); + cmd.push("--message-format=json"); + } + (_, _, _) => { + cmd.push("run"); + cmd.push("--message-format=json") + } } if mode == Release { @@ -575,8 +581,6 @@ fn build_execution_command( } } - cmd.push("--message-format=json"); - cmd } @@ -587,7 +591,6 @@ fn parse_json_output(output: Vec) -> Result<(String, String)> { let mut metadata_stream = cargo_metadata::Message::parse_stream(&output[..]); while let Some(msg) = metadata_stream.next() { - let message = msg.context(UnableToParseCargoOutputSnafu)?; match message { From 84c33ad40f2285ecdcc4af91c1e1314411663b94 Mon Sep 17 00:00:00 2001 From: hafeoz <95505675+hafeoz@users.noreply.github.com> Date: Tue, 11 Jan 2022 22:08:12 +0000 Subject: [PATCH 3/8] Fix clippy warnings --- ui/src/sandbox.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/sandbox.rs b/ui/src/sandbox.rs index 584814325..292efa8e4 100644 --- a/ui/src/sandbox.rs +++ b/ui/src/sandbox.rs @@ -590,7 +590,7 @@ fn parse_json_output(output: Vec) -> Result<(String, String)> { let mut metadata_stream = cargo_metadata::Message::parse_stream(&output[..]); - while let Some(msg) = metadata_stream.next() { + for msg in metadata_stream { let message = msg.context(UnableToParseCargoOutputSnafu)?; match message { From b25073a76d2503a41f2dc0269cc45f822a8349f9 Mon Sep 17 00:00:00 2001 From: hafeoz <95505675+hafeoz@users.noreply.github.com> Date: Tue, 11 Jan 2022 22:31:24 +0000 Subject: [PATCH 4/8] Fix warning --- ui/src/sandbox.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/sandbox.rs b/ui/src/sandbox.rs index 292efa8e4..62ce5a005 100644 --- a/ui/src/sandbox.rs +++ b/ui/src/sandbox.rs @@ -588,7 +588,7 @@ fn parse_json_output(output: Vec) -> Result<(String, String)> { let mut composed_stderr_string = String::new(); let mut composed_stdout_string = String::new(); - let mut metadata_stream = cargo_metadata::Message::parse_stream(&output[..]); + let metadata_stream = cargo_metadata::Message::parse_stream(&output[..]); for msg in metadata_stream { let message = msg.context(UnableToParseCargoOutputSnafu)?; From 60a42dc908ac5cd7f9a800e2779dd17aa5329ecc Mon Sep 17 00:00:00 2001 From: hafeoz <95505675+hafeoz@users.noreply.github.com> Date: Tue, 11 Jan 2022 22:36:04 +0000 Subject: [PATCH 5/8] Don't process children since they are already prerendered --- ui/src/sandbox.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ui/src/sandbox.rs b/ui/src/sandbox.rs index 62ce5a005..5fedea762 100644 --- a/ui/src/sandbox.rs +++ b/ui/src/sandbox.rs @@ -638,10 +638,6 @@ fn parse_diagnostic(diagnostic: cargo_metadata::diagnostic::Diagnostic) -> Strin )); } - for children in diagnostic.children { - diagnostic_string.push_str(&parse_diagnostic(children)); - } - diagnostic_string } From 04427ae0951e9f56b24d0d30985e198c046e4407 Mon Sep 17 00:00:00 2001 From: hafeoz <95505675+hafeoz@users.noreply.github.com> Date: Tue, 11 Jan 2022 22:55:08 +0000 Subject: [PATCH 6/8] Allow children to add spans --- ui/src/sandbox.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/ui/src/sandbox.rs b/ui/src/sandbox.rs index 5fedea762..c5977fb3b 100644 --- a/ui/src/sandbox.rs +++ b/ui/src/sandbox.rs @@ -602,7 +602,7 @@ fn parse_json_output(output: Vec) -> Result<(String, String)> { message, .. }) => { - composed_stderr_string.push_str(&parse_diagnostic(message)); + composed_stderr_string.push_str(&parse_diagnostic(message, true)); } _ => {} @@ -612,13 +612,18 @@ fn parse_json_output(output: Vec) -> Result<(String, String)> { Ok((composed_stdout_string, composed_stderr_string)) } -fn parse_diagnostic(diagnostic: cargo_metadata::diagnostic::Diagnostic) -> String { +fn parse_diagnostic( + diagnostic: cargo_metadata::diagnostic::Diagnostic, + should_output_message: bool, +) -> String { let mut diagnostic_string = String::new(); - if let Some(rendered_msg) = diagnostic.rendered { - diagnostic_string.push_str(&rendered_msg); - } else { - diagnostic_string.push_str(&diagnostic.message); + if should_output_message { + if let Some(rendered_msg) = diagnostic.rendered { + diagnostic_string.push_str(&rendered_msg); + } else { + diagnostic_string.push_str(&diagnostic.message); + } } for span in diagnostic.spans { @@ -638,6 +643,10 @@ fn parse_diagnostic(diagnostic: cargo_metadata::diagnostic::Diagnostic) -> Strin )); } + for children in diagnostic.children { + diagnostic_string.push_str(&parse_diagnostic(children, false)); + } + diagnostic_string } From 0aa59c3ab545b0a995814b78f6047b253ac8c1b2 Mon Sep 17 00:00:00 2001 From: hafeoz <95505675+hafeoz@users.noreply.github.com> Date: Wed, 12 Jan 2022 17:30:09 +0000 Subject: [PATCH 7/8] Fix linting --- ui/frontend/actions.ts | 5 +++-- ui/frontend/index.tsx | 3 ++- ui/frontend/reducers/code.ts | 16 +++++++++------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/ui/frontend/actions.ts b/ui/frontend/actions.ts index e26455e0e..622c10831 100644 --- a/ui/frontend/actions.ts +++ b/ui/frontend/actions.ts @@ -460,8 +460,9 @@ export const editCode = (code: string) => export const addMainFunction = () => createAction(ActionType.AddMainFunction); -export const applySuggestion = (startline: number, startcol: number, endline: number, endcol: number, suggestion: string) => - createAction(ActionType.ApplySuggestion, { startline, startcol, endline, endcol, suggestion }); +export const applySuggestion = + (startline: number, startcol: number, endline: number, endcol: number, suggestion: string) => + createAction(ActionType.ApplySuggestion, { startline, startcol, endline, endcol, suggestion }); export const enableFeatureGate = (featureGate: string) => createAction(ActionType.EnableFeatureGate, { featureGate }); diff --git a/ui/frontend/index.tsx b/ui/frontend/index.tsx index b6566dea2..b8c44b2ac 100644 --- a/ui/frontend/index.tsx +++ b/ui/frontend/index.tsx @@ -61,7 +61,8 @@ configureRustErrors({ enableFeatureGate: featureGate => store.dispatch(enableFeatureGate(featureGate)), gotoPosition: (line, col) => store.dispatch(gotoPosition(line, col)), selectText: (start, end) => store.dispatch(selectText(start, end)), - applySuggestion: (startline, startcol, endline, endcol, suggestion) => store.dispatch(applySuggestion(startline, startcol, endline, endcol, suggestion)), + applySuggestion: (startline, startcol, endline, endcol, suggestion) => + store.dispatch(applySuggestion(startline, startcol, endline, endcol, suggestion)), reExecuteWithBacktrace: () => store.dispatch(reExecuteWithBacktrace()), getChannel: () => store.getState().configuration.channel, }); diff --git a/ui/frontend/reducers/code.ts b/ui/frontend/reducers/code.ts index e216dcece..0e641fa42 100644 --- a/ui/frontend/reducers/code.ts +++ b/ui/frontend/reducers/code.ts @@ -20,13 +20,14 @@ export default function code(state = DEFAULT, action: Action): State { return `${state}\n\n${DEFAULT}`; case ActionType.ApplySuggestion: - let state_lines = state.split("\n"); - let startline = action.startline - 1; - let endline = action.endline - 1; - let startcol = action.startcol - 1; - let endcol = action.endcol - 1; + const state_lines = state.split('\n'); + const startline = action.startline - 1; + const endline = action.endline - 1; + const startcol = action.startcol - 1; + const endcol = action.endcol - 1; if (startline == endline) { - state_lines[startline] = state_lines[startline].substring(0, startcol) + state_lines[startline].substring(endcol); + state_lines[startline] = state_lines[startline].substring(0, startcol) + + state_lines[startline].substring(endcol); } else { if (state_lines.length > startline) { state_lines[startline] = state_lines[startline].substring(0, startcol); @@ -38,7 +39,8 @@ export default function code(state = DEFAULT, action: Action): State { state_lines.splice(startline + 1, endline - startline - 1); } } - state_lines[startline] = state_lines[startline].substring(0, startcol) + action.suggestion + state_lines[startline].substring(startcol); + state_lines[startline] = state_lines[startline].substring(0, startcol) + + action.suggestion + state_lines[startline].substring(startcol); state = state_lines.join('\n'); return state; From 8d3a0ff0a1981bc224a2aa5f546314095e7dd11f Mon Sep 17 00:00:00 2001 From: hafeoz <95505675+hafeoz@users.noreply.github.com> Date: Thu, 13 Jan 2022 03:00:46 +0000 Subject: [PATCH 8/8] Fix empty suggestion matching --- ui/frontend/highlighting.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/frontend/highlighting.ts b/ui/frontend/highlighting.ts index 9a7da4883..46a94543a 100644 --- a/ui/frontend/highlighting.ts +++ b/ui/frontend/highlighting.ts @@ -30,9 +30,9 @@ export function configureRustErrors({ }, 'error-location': /-->\s+(\/playground\/)?src\/.*\n/, 'import-suggestion-outer': { - pattern: /\[\[Line\s\d+\sCol\s\d+\s-\sLine\s\d+\sCol\s\d+:\s[.\s\S]+?\]\]/, + pattern: /\[\[Line\s\d+\sCol\s\d+\s-\sLine\s\d+\sCol\s\d+:\s[.\s\S]*?\]\]/, inside: { - 'import-suggestion': /\[\[Line\s\d+\sCol\s\d+\s-\sLine\s\d+\sCol\s\d+:\s[.\s\S]+?\]\]/, + 'import-suggestion': /\[\[Line\s\d+\sCol\s\d+\s-\sLine\s\d+\sCol\s\d+:\s[.\s\S]*?\]\]/, }, }, 'rust-errors-help': { @@ -87,7 +87,7 @@ export function configureRustErrors({ env.attributes['data-col'] = col; } if (env.type === 'import-suggestion') { - const errorMatch = /\[\[Line\s(\d+)\sCol\s(\d+)\s-\sLine\s(\d+)\sCol\s(\d+):\s([.\s\S]+?)\]\]/.exec(env.content); + const errorMatch = /\[\[Line\s(\d+)\sCol\s(\d+)\s-\sLine\s(\d+)\sCol\s(\d+):\s([.\s\S]*?)\]\]/.exec(env.content); const [_, startLine, startCol, endLine, endCol, importSuggestion] = errorMatch; env.tag = 'a'; env.attributes.href = '#';