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..622c10831 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,9 @@ 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 +842,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..46a94543a 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..b8c44b2ac 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,8 @@ 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..0e641fa42 100644 --- a/ui/frontend/reducers/code.ts +++ b/ui/frontend/reducers/code.ts @@ -19,8 +19,30 @@ 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: + 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); + } 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..c5977fb3b 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, }) } @@ -526,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 { @@ -571,6 +584,72 @@ fn build_execution_command( 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 metadata_stream = cargo_metadata::Message::parse_stream(&output[..]); + + for msg in metadata_stream { + 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, true)); + } + + _ => {} + } + } + + Ok((composed_stdout_string, composed_stderr_string)) +} + +fn parse_diagnostic( + diagnostic: cargo_metadata::diagnostic::Diagnostic, + should_output_message: bool, +) -> String { + let mut diagnostic_string = String::new(); + + 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 { + 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, false)); + } + + diagnostic_string +} + fn set_execution_environment( cmd: &mut Command, target: Option,