// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. // Wrapper refactor code action logic: generates a zero-parameter wrapper operation // that supplies default / placeholder values for an existing operation's parameters. #[cfg(test)] mod tests; use qsc::hir::{ CallableKind, ItemKind, PatKind, ty::{Prim, Ty}, }; use qsc::{ Span, line_column::{Encoding, Range}, }; use crate::{ compilation::Compilation, protocol::{CodeAction, CodeActionKind, TextEdit, WorkspaceEdit}, }; pub(crate) fn operation_refactors( compilation: &Compilation, source_name: &str, span: Span, encoding: Encoding, ) -> Vec { let mut code_actions = Vec::new(); let user_unit = compilation.user_unit(); let package = &user_unit.package; let source_map = &user_unit.sources; let source = source_map .find_by_name(source_name) .expect("source should exist"); let source_span = compilation.package_span_of_source(source_name); for (_, item) in package.items.iter() { if !source_span.contains(item.span.lo) || span.intersection(&item.span).is_none() { continue; } if let ItemKind::Callable(decl) = &item.kind { if decl.kind != CallableKind::Operation || decl.input.ty == Ty::UNIT || decl.name.name.starts_with(".lambda") { continue; // only operations with non-empty params } // Determine indentation using source-local offset (package offset minus source base). let local_lo = item.span.lo - source.offset; let indent = line_indentation(&source.contents, local_lo); let body_indent = if indent.contains('\t') { format!("{indent}\t") } else { format!("{indent} ") }; let original_name = decl.name.name.as_ref(); let wrapper_name = generate_unique_wrapper_name(package, original_name); let (decl_lines, call_args) = build_param_decls_and_call_args(&decl.input); let call_args_joined = if call_args.is_empty() { String::new() } else { call_args.join(", ") }; let return_ty = decl.output.display(); let return_is_unit = decl.output == Ty::UNIT; let call_line = if return_is_unit { format!("{body_indent}{original_name}({call_args_joined});") } else { format!("{body_indent}return {original_name}({call_args_joined});") }; let mut body_lines = Vec::new(); if !decl_lines.is_empty() { body_lines.push(format!( "{body_indent}// TODO: Fill out the values for the parameters" )); body_lines.extend(decl_lines.iter().map(|decl| format!("{body_indent}{decl}"))); body_lines.push(String::new()); // blank line } body_lines.push(format!("{body_indent}// Call original operation")); body_lines.push(call_line); // We intentionally do NOT prefix the first line with `indent` because the insertion point // inherits the existing line's leading whitespace. We DO append `{indent}` after the blank line // so that the original operation keeps its indentation after the inserted block. let newline = detect_newline(&source.contents, local_lo as usize); let wrapper_text = format!( "operation {wrapper_name}() : {return_ty} {{{newline}{}{newline}{indent}}}{newline}{newline}{indent}", body_lines.join(newline) ); // Insert immediately above the original operation: use zero-length span at item.span.lo let insert_span = Span { lo: local_lo, hi: local_lo, }; let edit_range = Range::from_span(encoding, &source.contents, &insert_span); code_actions.push(CodeAction { title: format!("Generate wrapper with default arguments for {original_name}"), edit: Some(WorkspaceEdit { changes: vec![( source_name.to_string(), vec![TextEdit { new_text: wrapper_text, range: edit_range, }], )], }), kind: Some(CodeActionKind::Refactor), is_preferred: None, }); } } code_actions } // Generate a wrapper name that does not clash with existing items in the same package (simple heuristic). fn generate_unique_wrapper_name(package: &qsc::hir::Package, base: &str) -> String { // New naming convention: WithDefaults, with numeric suffixes if needed. let mut candidate = format!("{base}WithDefaults"); let mut counter = 2; while package.items.iter().any(|(_, item)| match &item.kind { ItemKind::Callable(decl) => decl.name.name.as_ref() == candidate, _ => false, }) { candidate = format!("{base}WithDefaults{counter}"); counter += 1; } candidate } // Build declarations and call arguments preserving tuple structure. // Returns (declaration lines, call argument expressions list at top-level) fn build_param_decls_and_call_args(pat: &qsc::hir::Pat) -> (Vec, Vec) { let mut decls = Vec::new(); let call_args = match &pat.kind { PatKind::Tuple(items) => { let mut args = Vec::new(); for item in items { args.push(build_pattern_expr(item, &mut decls)); } args } _ => vec![build_pattern_expr(pat, &mut decls)], }; (decls, call_args) } // Recursively build an expression for a pattern, pushing any needed declarations (let/use) into decls. fn build_pattern_expr(pat: &qsc::hir::Pat, decls: &mut Vec) -> String { match &pat.kind { PatKind::Err | PatKind::Discard => "_".to_string(), PatKind::Tuple(items) => { let parts: Vec = items.iter().map(|p| build_pattern_expr(p, decls)).collect(); format!("({})", parts.join(", ")) } PatKind::Bind(ident) => build_binding_expr(ident.name.as_ref(), &pat.ty, decls), } } fn build_binding_expr(name: &str, ty: &Ty, decls: &mut Vec) -> String { match ty { Ty::Prim(Prim::Qubit) => { decls.push(format!("use {name} = Qubit();")); name.to_string() } Ty::Array(inner) if matches!(**inner, Ty::Prim(Prim::Qubit)) => { decls.push(format!("use {name} = Qubit[1];")); name.to_string() } Ty::Tuple(items) => { let mut qubit_counter = 0u32; let mut qubit_reg_counter = 0u32; let mut deferred_todos = Vec::new(); let tuple_expr = build_tuple_literal( name, items, decls, &mut qubit_counter, &mut qubit_reg_counter, &mut deferred_todos, ); // Place any deferred TODO comments before the binding so they aren't interleaved with allocations. decls.extend(deferred_todos); decls.push(format!("let {name} = {tuple_expr};")); name.to_string() } _ => { let (default_expr, comment) = default_value_for_type(ty); if let Some(expr) = default_expr { decls.push(format!("let {name} = {expr};")); name.to_string() } else { decls.push(format!("// TODO: provide value for {name} ({comment})")); "_".to_string() } } } } // Build a tuple literal expression for a list of types, adding declarations for qubits / complex components. fn build_tuple_literal( base: &str, items: &[Ty], decls: &mut Vec, qubit_counter: &mut u32, qubit_reg_counter: &mut u32, deferred_todos: &mut Vec, ) -> String { if items.is_empty() { return "()".to_string(); } let mut parts = Vec::new(); for ty in items { match ty { Ty::Prim(Prim::Qubit) => { let v = format!("{base}_q{qubit_counter}"); *qubit_counter += 1; decls.push(format!("use {v} = Qubit();")); parts.push(v); } Ty::Array(inner) if matches!(**inner, Ty::Prim(Prim::Qubit)) => { let v = format!("{base}_qs{qubit_reg_counter}"); *qubit_reg_counter += 1; decls.push(format!("use {v} = Qubit[1];")); parts.push(v); } Ty::Tuple(sub) => { let nested = build_tuple_literal( base, sub, decls, qubit_counter, qubit_reg_counter, deferred_todos, ); parts.push(nested); } _ => { let (default_expr, comment) = default_value_for_type(ty); if let Some(expr) = default_expr { parts.push(expr); } else { deferred_todos.push(format!( "// TODO: provide value for tuple component of {base} ({comment})" )); parts.push("_".to_string()); } } } } if parts.len() == 1 { format!("({},)", parts[0]) } else { format!("({})", parts.join(", ")) } } fn default_value_for_type(ty: &Ty) -> (Option, String) { match ty { Ty::Prim(p) => match p { Prim::Int => (Some("0".to_string()), "Int".to_string()), Prim::Bool => (Some("false".to_string()), "Bool".to_string()), Prim::Double => (Some("0.0".to_string()), "Double".to_string()), Prim::Result => (Some("Zero".to_string()), "Result".to_string()), Prim::Pauli => (Some("PauliI".to_string()), "Pauli".to_string()), Prim::BigInt => (Some("0L".to_string()), "BigInt".to_string()), Prim::String => (Some("\"\"".to_string()), "String".to_string()), Prim::Qubit => (None, "Qubit - allocate with 'use'".to_string()), Prim::Range | Prim::RangeTo | Prim::RangeFrom | Prim::RangeFull => { (Some("0..1".to_string()), "Range".to_string()) } }, Ty::Array(_) => (Some("[]".to_string()), "Array".to_string()), Ty::Tuple(_) => (None, "Tuple".to_string()), Ty::Param { name, .. } => (None, format!("Generic parameter {name}")), Ty::Udt(name, _) => (None, format!("UDT {name}")), Ty::Arrow(_) => (None, "Callable type".to_string()), Ty::Infer(_) | Ty::Err => (None, "Unknown".to_string()), } } fn line_indentation(contents: &str, offset: u32) -> String { let offset_usize = offset as usize; let line_start = contents[..offset_usize] .rfind('\n') .map_or(0, |idx| idx + 1); contents[line_start..offset_usize] .chars() .take_while(|c| *c == ' ' || *c == '\t') .collect() } // Detect the newline sequence to use when inserting text. Preference order: // 1. The last newline sequence before the target offset. // 2. The first newline sequence in the file (if none before offset). // 3. Fallback to '\n'. Supports '\n' and '\r\n'. fn detect_newline(contents: &str, op_offset: usize) -> &'static str { // Helper to examine a position of a '\n' and decide if it's part of a CRLF pair. let classify = |idx: usize| { if idx > 0 && contents.as_bytes()[idx - 1] == b'\r' { "\r\n" } else { "\n" } }; if op_offset <= contents.len() && let Some(prev_nl) = contents[..op_offset].rfind('\n') { return classify(prev_nl); } if let Some(first_nl) = contents.find('\n') { return classify(first_nl); } "\n" }