// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

// cSpell: ignore newtype CROSSFILE

//! `.slint` rename support, plus the classifier that decides when a rename
//! should be paired with a Rust/C++ host-language rewrite.
//!
//! # Host-language follow-up flow
//!
//! Renaming a public property/callback/function in `.slint` can optionally
//! also replace matching generated Rust/C++ accessor identifiers (`get_<n>`,
//! `set_<n>`, `invoke_<n>`, `on_<n>`) throughout the workspace. The
//! `textDocument/rename` handler in `language.rs` returns the slint-only
//! edits synchronously; the host-language follow-up runs in a `spawn_local`
//! task because the rename handler is sync but the dialog and the second
//! `workspace/applyEdit` are async.
//!
//! After the synchronous slint rename, the spawned task:
//!
//! 1. Calls [`DeclarationNode::host_language_classification`]. This walks
//!    every loaded `Document` (via `DocumentCache::all_documents()`) and,
//!    for each exported non-interface component, follows the `inherits`
//!    chain looking for a declaration whose syntax node matches the rename
//!    target. The walk is bounded (depth + visited-set) so a cyclic
//!    `inherits` chain from a mid-edit state can't hang the task. Returns
//!    the declaration kind and current name -- or `None` if the
//!    declaration isn't reachable from any exported component or its
//!    visibility/type rules it out.
//! 2. Skips the dialog entirely when
//!    `normalize_identifier(new_name) == old_name` (kebab/snake no-op
//!    that produces no accessor change).
//! 3. Checks an in-memory "don't ask again" flag that suppresses the dialog
//!    for the rest of the session. TODO(#12111): Persist this setting across
//!    sessions.
//! 4. Sends `window/showMessageRequest` with three actions: *Search & Replace
//!    Rust/C++ accessors*, *Skip*, *Skip and don't ask again*. Dismissal is
//!    treated as Skip.
//! 5. On *Search & Replace*, queries `workspace/workspaceFolders` (or falls back
//!    to the cached `InitializeParams` when the client doesn't support
//!    the query), runs
//!    [`crate::common::host_language_search::search_replace_host_language_accessors`]
//!    against every folder, and sends a second `workspace/applyEdit` with
//!    the host-language edits.
//! 6. Reports the outcome via `window/showMessage`: count of replaced
//!    occurrences on success, or a warning on scan failure / refused edit.
//!
//! The slint rename and the host-language rewrite arrive as two
//! independent edits; the editor's undo treats them separately. That is
//! intentional: if the user rejects the host-language rewrite or the
//! scanner errors, the slint rename still stands.
//!
//! Failure modes (no workspace folders, scan exceeds `max_files`, client
//! refuses the applyEdit) **soft-degrade**: tracing warning + a
//! `window/showMessage` to the user, but the slint rename is not rolled
//! back.

use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;

use crate::{common, util};

use i_slint_compiler::diagnostics::{SourceFile, Spanned};
use i_slint_compiler::generator::accessor_names::DeclarationKind;
use i_slint_compiler::object_tree;
use i_slint_compiler::parser::{SyntaxKind, SyntaxNode, SyntaxToken, syntax_nodes};
use lsp_types::Url;
use smol_str::SmolStr;

#[cfg(target_arch = "wasm32")]
use crate::wasm_prelude::*;

pub fn main_identifier(input: &SyntaxNode) -> Option<SyntaxToken> {
    input.child_token(SyntaxKind::Identifier)
}

fn is_symbol_name_exported(
    document_cache: &common::DocumentCache,
    document_node: &syntax_nodes::Document,
    query: &DeclarationNodeQuery,
) -> Option<SmolStr> {
    let ti = query.parent_token_info();

    for export in document_node.ExportsList() {
        for specifier in export.ExportSpecifier() {
            let external = specifier
                .ExportName()
                .and_then(|en| i_slint_compiler::parser::identifier_text(&en));

            let export_id = specifier.ExportIdentifier();
            let export_id_str = i_slint_compiler::parser::identifier_text(&export_id);

            if export_id_str.as_ref() == Some(&ti.name)
                && ti.is_same_symbol(document_cache, main_identifier(&export_id).unwrap())
            {
                return external.or(export_id_str);
            }
        }
        if let Some(component) = export.Component() {
            let identifier = component.DeclaredIdentifier();
            let identifier_str = i_slint_compiler::parser::identifier_text(&identifier);

            if identifier_str.as_ref() == Some(&ti.name)
                && ti.is_same_symbol(document_cache, main_identifier(&identifier).unwrap())
            {
                return identifier_str;
            }
        }
        for structs in export.StructDeclaration() {
            let identifier = structs.DeclaredIdentifier();
            let identifier_str = i_slint_compiler::parser::identifier_text(&identifier);

            if identifier_str.as_ref() == Some(&ti.name)
                && ti.is_same_symbol(document_cache, main_identifier(&identifier).unwrap())
            {
                return identifier_str;
            }
        }
        for enums in export.EnumDeclaration() {
            let enum_name = i_slint_compiler::parser::identifier_text(&enums.DeclaredIdentifier());
            if enum_name.as_ref() == Some(&ti.name) {
                return enum_name;
            }
        }
    }

    None
}

fn fix_imports(
    document_cache: &common::DocumentCache,
    query: &DeclarationNodeQuery,
    exporter_path: &Path,
    new_type: &str,
    edits: &mut Vec<common::SingleTextEdit>,
) {
    let Ok(exporter_url) = Url::from_file_path(exporter_path) else {
        return;
    };
    for (url, doc) in document_cache.all_url_documents() {
        if url.scheme() == "builtin" || url.path() == exporter_url.path() {
            continue;
        }

        fix_import_in_document(document_cache, query, doc, exporter_path, new_type, edits);
    }
}

fn import_path(document_directory: &Path, specifier: &SyntaxNode) -> Option<PathBuf> {
    assert!([SyntaxKind::ImportSpecifier, SyntaxKind::ExportModule].contains(&specifier.kind()));

    let import = specifier
        .child_token(SyntaxKind::StringLiteral)
        .and_then(|t| i_slint_compiler::literals::unescape_string(t.text()))?;

    if import == "std-widgets.slint" || import.starts_with("@") {
        return None; // No need to ever look at this!
    }

    // Do not bother with the TypeLoader: It will check the FS, which we do not use:-/
    Some(i_slint_compiler::pathutils::clean_path(&document_directory.join(import)))
}

/// Fix up `Type as OtherType` like specifiers found in import and export lists
fn fix_specifier(
    document_cache: &common::DocumentCache,
    query: &DeclarationNodeQuery,
    new_type: &str,
    type_name: SyntaxToken,
    renamed_to: Option<SyntaxToken>,
    edits: &mut Vec<common::SingleTextEdit>,
) -> Option<DeclarationNodeQuery> {
    fn replace_x_as_y_with_newtype(
        document_cache: &common::DocumentCache,
        source_file: &i_slint_compiler::diagnostics::SourceFile,
        x: &SyntaxToken,
        y: &SyntaxToken,
        new_type: &str,
    ) -> common::SingleTextEdit {
        let start_position = util::text_size_to_lsp_position(
            source_file,
            x.text_range().start(),
            document_cache.format,
        );
        let end_position = util::text_size_to_lsp_position(
            source_file,
            y.text_range().end(),
            document_cache.format,
        );
        common::SingleTextEdit::from_path(
            document_cache,
            source_file.path(),
            lsp_types::TextEdit {
                range: lsp_types::Range::new(start_position, end_position),
                new_text: new_type.to_string(),
            },
        )
        .expect("URL conversion can not fail here")
    }

    let ti = query.parent_token_info();
    let source_file = type_name.source_file()?;

    if i_slint_compiler::parser::normalize_identifier(type_name.text()) == ti.name {
        if let Some(renamed_to) = renamed_to {
            if i_slint_compiler::parser::normalize_identifier(renamed_to.text())
                == i_slint_compiler::parser::normalize_identifier(new_type)
            {
                if query.has_parent_token_info() {
                    return query.update_parent_token_info(renamed_to);
                }

                // `Old as New` => `New`
                edits.push(replace_x_as_y_with_newtype(
                    document_cache,
                    source_file,
                    &type_name,
                    &renamed_to,
                    new_type,
                ));
            } else {
                if query.has_parent_token_info() {
                    return query.update_parent_token_info(renamed_to);
                }

                // `Old as Foo` => `New as Foo`
                edits.push(
                    common::SingleTextEdit::from_path(
                        document_cache,
                        source_file.path(),
                        lsp_types::TextEdit {
                            range: util::token_to_lsp_range(&type_name, document_cache.format),
                            new_text: new_type.to_string(),
                        },
                    )
                    .expect("URL conversion can not fail here"),
                );
            }
            // Nothing else to change: We still use the old name everywhere.
            return None;
        } else {
            if query.has_parent_token_info() {
                return Some(query.clone());
            }

            // `Old` => `New`
            edits.push(
                common::SingleTextEdit::from_path(
                    document_cache,
                    source_file.path(),
                    lsp_types::TextEdit {
                        range: util::token_to_lsp_range(&type_name, document_cache.format),
                        new_text: new_type.to_string(),
                    },
                )
                .expect("URL conversion can not fail here"),
            );
        }

        return query.update_parent_token_info(type_name);
    }
    if let Some(renamed_to) = renamed_to
        && i_slint_compiler::parser::normalize_identifier(renamed_to.text()) == ti.name
    {
        if i_slint_compiler::parser::normalize_identifier(type_name.text())
            == i_slint_compiler::parser::normalize_identifier(new_type)
        {
            if query.has_parent_token_info() {
                return Some(query.clone());
            }

            // `New as Old` => `New`
            edits.push(replace_x_as_y_with_newtype(
                document_cache,
                source_file,
                &type_name,
                &renamed_to,
                new_type,
            ));
        } else {
            // `Foo as Old` => `Foo as New`
            if query.has_parent_token_info() {
                return None;
            }

            edits.push(
                common::SingleTextEdit::from_path(
                    document_cache,
                    source_file.path(),
                    lsp_types::TextEdit {
                        range: util::token_to_lsp_range(&renamed_to, document_cache.format),
                        new_text: new_type.to_string(),
                    },
                )
                .expect("URL conversion can not fail here"),
            );
        }
        return query.update_parent_token_info(renamed_to);
    }

    None
}

fn fix_import_in_document(
    document_cache: &common::DocumentCache,
    query: &DeclarationNodeQuery,
    document_node: &syntax_nodes::Document,
    exporter_path: &Path,
    new_type: &str,
    edits: &mut Vec<common::SingleTextEdit>,
) {
    let Some(document_directory) =
        document_node.source_file().and_then(|sf| sf.path().parent()).map(|p| p.to_owned())
    else {
        return;
    };

    for import_specifier in document_node.ImportSpecifier() {
        let Some(import_path) = import_path(&document_directory, &import_specifier) else {
            continue;
        };

        if import_path != exporter_path {
            continue;
        }

        let Some(list) = import_specifier.ImportIdentifierList() else {
            continue;
        };

        for identifier in list.ImportIdentifier() {
            if let Some(sub_query) = fix_specifier(
                document_cache,
                query,
                new_type,
                main_identifier(&identifier.ExternalName()).unwrap(),
                identifier.InternalName().map(|internal| main_identifier(&internal).unwrap()),
                edits,
            ) {
                // Change exports
                fix_export_lists(document_cache, document_node, &sub_query, new_type, edits);

                // Change all local usages:
                rename_local_symbols(document_cache, document_node, &sub_query, new_type, edits);
            }
        }
    }

    // Find export modules!
    for export_item in document_node.ExportsList() {
        let Some(module) = export_item.ExportModule() else {
            continue;
        };

        let Some(import_path) = import_path(&document_directory, &module) else {
            continue;
        };

        if import_path != exporter_path {
            continue;
        }

        if module.child_token(SyntaxKind::Star).is_some() {
            // Change upstream imports
            fix_imports(document_cache, query, document_node.source_file.path(), new_type, edits);
        } else {
            for specifier in export_item.ExportSpecifier() {
                if let Some(sub_query) = fix_specifier(
                    document_cache,
                    query,
                    new_type,
                    main_identifier(&specifier.ExportIdentifier()).unwrap(),
                    specifier.ExportName().map(|export| main_identifier(&export).unwrap()),
                    edits,
                ) {
                    // Change upstream imports
                    fix_imports(
                        document_cache,
                        &sub_query,
                        document_node.source_file.path(),
                        new_type,
                        edits,
                    );

                    // Change all local usages:
                    rename_local_symbols(
                        document_cache,
                        document_node,
                        &sub_query,
                        new_type,
                        edits,
                    );
                }
            }
        }
    }
}

fn fix_export_lists(
    document_cache: &common::DocumentCache,
    document_node: &syntax_nodes::Document,
    query: &DeclarationNodeQuery,
    new_type: &str,
    edits: &mut Vec<common::SingleTextEdit>,
) -> Option<SmolStr> {
    for export in document_node.ExportsList() {
        if export.ExportModule().is_some() {
            // Import already covers these!
            continue;
        }

        for specifier in export.ExportSpecifier() {
            if let Some(sub_query) = fix_specifier(
                document_cache,
                query,
                new_type,
                main_identifier(&specifier.ExportIdentifier()).unwrap(),
                specifier.ExportName().and_then(|en| main_identifier(&en)),
                edits,
            ) {
                let my_path = document_node.source_file.path();
                fix_imports(document_cache, &sub_query, my_path, new_type, edits);
                return Some(sub_query.token_info.name.clone());
            }
        }
    }
    None
}

/// Rename all local non import/export related identifiers
fn rename_local_symbols(
    document_cache: &common::DocumentCache,
    document_node: &syntax_nodes::Document,
    query: &DeclarationNodeQuery,
    new_type: &str,
    edits: &mut Vec<common::SingleTextEdit>,
) {
    let ti = &query.token_info;

    let mut current_token = document_node.first_token();
    while let Some(current) = current_token {
        if current.kind() == SyntaxKind::Identifier
            && i_slint_compiler::parser::normalize_identifier(current.text()) == ti.name
            && !matches!(
                current.parent().kind(),
                SyntaxKind::ExternalName
                    | SyntaxKind::InternalName
                    | SyntaxKind::ExportIdentifier
                    | SyntaxKind::ExportName
                    | SyntaxKind::PropertyDeclaration
            )
            && ti.is_same_symbol(document_cache, current.clone())
        {
            edits.push(
                common::SingleTextEdit::from_path(
                    document_cache,
                    current.source_file.path(),
                    lsp_types::TextEdit {
                        range: util::token_to_lsp_range(&current, document_cache.format),
                        new_text: new_type.to_string(),
                    },
                )
                .expect("URL conversion can not fail here"),
            )
        }

        current_token = current.next_token();
    }
}

/// Rename an InternalName in an import statement
///
/// The ExternalName is different from our name, which is why we ended up here.
///
/// Change the InternalName, fix up local usage and then fix up exports. If exports
/// change something, also fix all the necessary imports.
fn rename_internal_name(
    document_cache: &common::DocumentCache,
    query: &DeclarationNodeQuery,
    internal_name: &syntax_nodes::InternalName,
    new_type: &str,
) -> lsp_types::WorkspaceEdit {
    let Some(document) = document_cache.get_document_for_source_file(&internal_name.source_file)
    else {
        return Default::default();
    };
    let Some(document_node) = &document.node else {
        return Default::default();
    };

    let mut edits = Vec::new();

    let parent: syntax_nodes::ImportIdentifier = internal_name.parent().unwrap().into();
    let external_name = parent.ExternalName();

    if let Some(sub_query) = fix_specifier(
        document_cache,
        query,
        new_type,
        main_identifier(&external_name).unwrap(),
        main_identifier(internal_name),
        &mut edits,
    ) {
        rename_local_symbols(document_cache, document_node, &sub_query, new_type, &mut edits);
        fix_export_lists(document_cache, document_node, &sub_query, new_type, &mut edits);
    }

    common::create_workspace_edit_from_single_text_edits(edits)
}

/// We ended up in an ExportName that we need to rename.
///
/// The internal name is different, otherwise we would not have ended up here:-)
/// So we need to rename the export itself and then fix up imports.
fn rename_export_name(
    document_cache: &common::DocumentCache,
    query: &DeclarationNodeQuery,
    export_name: &syntax_nodes::ExportName,
    new_type: &str,
) -> lsp_types::WorkspaceEdit {
    let mut edits = Vec::new();

    let specifier: syntax_nodes::ExportSpecifier = export_name.parent().unwrap().into();
    let Some(internal_name) = main_identifier(&specifier.ExportIdentifier()) else {
        return Default::default();
    };

    if let Some(sub_query) = fix_specifier(
        document_cache,
        query,
        new_type,
        internal_name,
        main_identifier(export_name),
        &mut edits,
    ) {
        // Change exports
        fix_imports(
            document_cache,
            &sub_query,
            export_name.source_file.path(),
            new_type,
            &mut edits,
        );
    };

    common::create_workspace_edit_from_single_text_edits(edits)
}

#[derive(Clone, Debug)]
pub enum DeclarationNodeKind {
    DeclaredIdentifier(syntax_nodes::DeclaredIdentifier),
    InternalName(syntax_nodes::InternalName),
    ExportName(syntax_nodes::ExportName),
}

#[derive(Clone, Debug)]
pub struct DeclarationNode {
    kind: DeclarationNodeKind,
    query: DeclarationNodeQuery,
}

pub fn find_declaration_node(
    document_cache: &common::DocumentCache,
    token: &SyntaxToken,
) -> Option<DeclarationNode> {
    if token.kind() != SyntaxKind::Identifier {
        return None;
    }

    DeclarationNodeQuery::new(document_cache, token.clone())?.find_declaration_node(document_cache)
}

impl DeclarationNode {
    pub fn rename(
        &self,
        document_cache: &common::DocumentCache,
        new_type: &str,
    ) -> crate::Result<lsp_types::WorkspaceEdit> {
        match &self.kind {
            DeclarationNodeKind::DeclaredIdentifier(id) => {
                rename_declared_identifier(document_cache, &self.query, id, new_type)
            }
            DeclarationNodeKind::InternalName(internal) => {
                Ok(rename_internal_name(document_cache, &self.query, internal, new_type))
            }
            DeclarationNodeKind::ExportName(export) => {
                Ok(rename_export_name(document_cache, &self.query, export, new_type))
            }
        }
    }

    /// If this declaration would be exposed in the generated Rust/C++ public
    /// API, return the information the host-language scanner needs to find
    /// matching accessor identifiers in Rust and C++ source files.
    ///
    /// Returns `None` for declarations that don't emit Rust/C++ accessors:
    /// private properties, declarations inside non-exported inner components,
    /// structs, enums, components themselves, etc.
    ///
    /// Mirrors the logic of `i_slint_compiler::passes::check_public_api`
    /// rather than reading `expose_in_public_api`: the LSP only runs
    /// `run_import_passes`, so that flag is never set in this context.
    pub fn host_language_classification(
        &self,
        document_cache: &common::DocumentCache,
    ) -> Option<HostLanguageRenameInfo> {
        let DeclarationNodeKind::DeclaredIdentifier(decl_id) = &self.kind else {
            return None;
        };

        let parent = decl_id.parent()?;
        let kind = match parent.kind() {
            SyntaxKind::PropertyDeclaration => DeclarationKind::Property,
            SyntaxKind::CallbackDeclaration => DeclarationKind::Callback,
            SyntaxKind::Function => DeclarationKind::Function,
            _ => return None,
        };

        let name = i_slint_compiler::parser::identifier_text(decl_id)?;
        let source_file = decl_id.source_file.clone();
        // Ensure the source file is known to the LSP -- avoids classifying
        // declarations from documents the cache hasn't loaded.
        document_cache.get_document_for_source_file(&source_file)?;

        // The LSP only runs `run_import_passes`, not the full pipeline; the
        // `check_public_api` pass that sets `expose_in_public_api` never runs
        // here, and `inlining` (which would copy inherited declarations onto
        // the exported component's root_element) is also absent. Compute the
        // public-API exposure ourselves: the declaration must be reachable
        // from an exported, non-interface component's root_element (walking
        // its `inherits` chain), the type must be `ok_for_public_api`, and
        // the visibility must not be `private`. Mirrors the gating logic of
        // `passes::check_public_api`.
        //
        // Iterate every loaded document, not just the one containing the
        // renamed declaration: a non-exported base in file A can be
        // inherited by an exported component in file B, and the codegen for
        // B WILL emit accessors for the inherited declaration.
        for doc in document_cache.all_documents() {
            for component in doc.exports.iter().filter_map(|(_, e)| e.as_ref().left()) {
                if component.is_interface() {
                    // Interfaces don't generate Rust/C++ accessors.
                    continue;
                }
                if matching_decl_in_inheritance_chain(component, &name, &parent, &source_file) {
                    return Some(HostLanguageRenameInfo { kind, old_name: name });
                }
            }
        }
        None
    }
}

/// Walk `component.root_element` and its `inherits` chain looking for a
/// declaration whose syntax node matches `declaration_node` (the syntax
/// node of the `PropertyDeclaration` / `CallbackDeclaration` / `Function`
/// being renamed), and which would be exposed in the generated Rust/C++
/// public API: non-private visibility and a type that `ok_for_public_api`.
///
/// Bounded by a fixed depth and by a visited-set on `Rc::as_ptr`: in a
/// healthy program `inherits` never cycles, but the LSP runs only
/// `run_import_passes` and may see mid-edit states where the cycle check
/// hasn't run yet. We must not hang the synchronous rename handler.
fn matching_decl_in_inheritance_chain(
    component: &Rc<object_tree::Component>,
    name: &SmolStr,
    declaration_node: &SyntaxNode,
    source_file: &SourceFile,
) -> bool {
    const MAX_DEPTH: usize = 64;
    let mut element = component.root_element.clone();
    let mut visited: std::collections::HashSet<usize> = std::collections::HashSet::new();
    for _ in 0..MAX_DEPTH {
        if !visited.insert(Rc::as_ptr(&element) as usize) {
            return false; // cycle in `inherits` chain
        }
        let element_borrow = element.borrow();
        if let Some(decl) = element_borrow.property_declarations.get(name)
            && let Some(node) = decl.node.as_ref()
            && node.text_range() == declaration_node.text_range()
            && Arc::ptr_eq(&node.source_file, source_file)
        {
            if decl.visibility == object_tree::PropertyVisibility::Private {
                return false;
            }
            if !decl.property_type.ok_for_public_api() {
                return false;
            }
            return true;
        }
        let base = element_borrow.base_type.clone();
        drop(element_borrow);
        match base {
            i_slint_compiler::langtype::ElementType::Component(c) => {
                element = c.root_element.clone();
            }
            _ => return false,
        }
    }
    false
}

/// Information needed by the host-language scanner to compute Rust/C++
/// accessor-rename edits to pair with a Slint property/callback/function
/// rename.
#[derive(Clone, Debug)]
pub struct HostLanguageRenameInfo {
    /// Whether the renamed declaration is a property, callback, or function.
    /// Determines which accessor prefixes (`get_`/`set_`/`invoke_`/`on_`) the
    /// scanner searches for.
    pub kind: DeclarationKind,
    /// The current (pre-rename) Slint name. The scanner derives the old
    /// accessor names from this via
    /// [`i_slint_compiler::generator::accessor_names`].
    pub old_name: SmolStr,
}

fn find_last_declared_identifier_at_or_before(
    token: SyntaxToken,
    type_name: &SmolStr,
) -> Option<SyntaxNode> {
    let mut token = Some(token);

    while let Some(t) = token {
        if t.kind() == SyntaxKind::Identifier {
            let node = t.parent();
            if node.kind() == SyntaxKind::DeclaredIdentifier
                && i_slint_compiler::parser::identifier_text(&node).as_ref() == Some(type_name)
            {
                return Some(node);
            }
        }
        token = t.prev_token();
    }

    None
}

#[derive(Clone, Debug)]
struct TokenInformation {
    info: common::token_info::TokenInfo,
    name: SmolStr,
    token: SyntaxToken,
}

impl TokenInformation {
    fn is_same_symbol(&self, document_cache: &common::DocumentCache, token: SyntaxToken) -> bool {
        let Some(info) = common::token_info::token_info(document_cache, token.clone()) else {
            return false;
        };

        fn check_element(
            element: &i_slint_compiler::object_tree::ElementRc,
            node: &SyntaxNode,
        ) -> bool {
            if element.borrow().debug.iter().any(|di| {
                Arc::ptr_eq(&di.node.source_file, &node.source_file)
                    && di.node.text_range() == node.text_range()
            }) {
                return true;
            }
            if let i_slint_compiler::langtype::ElementType::Component(c) =
                &element.borrow().base_type
            {
                check_element(&c.root_element, node)
            } else {
                false
            }
        }

        match (&self.info, &info) {
            (common::token_info::TokenInfo::Type(s), common::token_info::TokenInfo::Type(o)) => {
                s == o
            }
            (
                common::token_info::TokenInfo::ElementType(s),
                common::token_info::TokenInfo::ElementType(o),
            ) => s == o,
            (
                common::token_info::TokenInfo::ElementRc(s),
                common::token_info::TokenInfo::ElementRc(o),
            ) => Rc::ptr_eq(s, o),
            (
                common::token_info::TokenInfo::LocalProperty(s),
                common::token_info::TokenInfo::LocalProperty(o),
            ) => Arc::ptr_eq(&s.source_file, &o.source_file) && s.text_range() == o.text_range(),
            (
                common::token_info::TokenInfo::NamedReference(nl),
                common::token_info::TokenInfo::NamedReference(nr),
            ) => Rc::ptr_eq(&nl.element(), &nr.element()) && nl.name() == nr.name(),
            (
                common::token_info::TokenInfo::ElementType(
                    i_slint_compiler::langtype::ElementType::Component(c),
                ),
                common::token_info::TokenInfo::ElementRc(e),
            )
            | (
                common::token_info::TokenInfo::ElementRc(e),
                common::token_info::TokenInfo::ElementType(
                    i_slint_compiler::langtype::ElementType::Component(c),
                ),
            ) => {
                if let Some(ce) = c.node.as_ref().map(|cn| cn.Element()) {
                    e.borrow().debug.iter().any(|di| {
                        Some(di.node.source_file.path()) == ce.source_file().map(|sf| sf.path())
                            && di.node.text_range() == ce.text_range()
                    })
                } else {
                    false
                }
            }
            (
                common::token_info::TokenInfo::NamedReference(nr),
                common::token_info::TokenInfo::LocalProperty(s),
            )
            | (
                common::token_info::TokenInfo::LocalProperty(s),
                common::token_info::TokenInfo::NamedReference(nr),
            ) => {
                s.parent().is_some_and(|n| check_element(&nr.element(), &n))
                    && i_slint_compiler::parser::identifier_text(&s.DeclaredIdentifier())
                        .is_some_and(|x| &x == nr.name())
            }
            (
                common::token_info::TokenInfo::LocalProperty(s),
                common::token_info::TokenInfo::IncompleteNamedReference(nr1, nr2),
            )
            | (
                common::token_info::TokenInfo::IncompleteNamedReference(nr1, nr2),
                common::token_info::TokenInfo::LocalProperty(s),
            ) => {
                matches!(nr1, i_slint_compiler::langtype::ElementType::Component(c) if s.parent().is_some_and(|n| check_element(&c.root_element, &n)))
                    && Some(nr2)
                        == i_slint_compiler::parser::identifier_text(&s.DeclaredIdentifier())
                            .as_ref()
            }
            (
                common::token_info::TokenInfo::LocalCallback(s),
                common::token_info::TokenInfo::LocalCallback(o),
            ) => Arc::ptr_eq(&s.source_file, &o.source_file) && s.text_range() == o.text_range(),
            (
                common::token_info::TokenInfo::NamedReference(nr),
                common::token_info::TokenInfo::LocalCallback(s),
            )
            | (
                common::token_info::TokenInfo::LocalCallback(s),
                common::token_info::TokenInfo::NamedReference(nr),
            ) => {
                s.parent().is_some_and(|n| check_element(&nr.element(), &n))
                    && i_slint_compiler::parser::identifier_text(&s.DeclaredIdentifier())
                        .is_some_and(|x| &x == nr.name())
            }
            (
                common::token_info::TokenInfo::LocalCallback(s),
                common::token_info::TokenInfo::IncompleteNamedReference(nr1, nr2),
            )
            | (
                common::token_info::TokenInfo::IncompleteNamedReference(nr1, nr2),
                common::token_info::TokenInfo::LocalCallback(s),
            ) => {
                matches!(nr1, i_slint_compiler::langtype::ElementType::Component(c) if s.parent().is_some_and(|n| check_element(&c.root_element, &n)))
                    && Some(nr2)
                        == i_slint_compiler::parser::identifier_text(&s.DeclaredIdentifier())
                            .as_ref()
            }
            (
                common::token_info::TokenInfo::LocalFunction(s),
                common::token_info::TokenInfo::LocalFunction(o),
            ) => Arc::ptr_eq(&s.source_file, &o.source_file) && s.text_range() == o.text_range(),
            (
                common::token_info::TokenInfo::NamedReference(nr),
                common::token_info::TokenInfo::LocalFunction(s),
            )
            | (
                common::token_info::TokenInfo::LocalFunction(s),
                common::token_info::TokenInfo::NamedReference(nr),
            ) => {
                s.parent().is_some_and(|n| check_element(&nr.element(), &n))
                    && i_slint_compiler::parser::identifier_text(&s.DeclaredIdentifier())
                        .is_some_and(|x| &x == nr.name())
            }
            (
                common::token_info::TokenInfo::LocalFunction(s),
                common::token_info::TokenInfo::IncompleteNamedReference(nr1, nr2),
            )
            | (
                common::token_info::TokenInfo::IncompleteNamedReference(nr1, nr2),
                common::token_info::TokenInfo::LocalFunction(s),
            ) => {
                matches!(nr1, i_slint_compiler::langtype::ElementType::Component(c) if s.parent().is_some_and(|n| check_element(&c.root_element, &n)))
                    && Some(nr2)
                        == i_slint_compiler::parser::identifier_text(&s.DeclaredIdentifier())
                            .as_ref()
            }
            (_, _) => false,
        }
    }
}

#[derive(Clone, Debug)]
struct DeclarationNodeQuery {
    token_info: TokenInformation,
    parent_token_info: Option<TokenInformation>,
}

impl DeclarationNodeQuery {
    fn new(document_cache: &common::DocumentCache, token: SyntaxToken) -> Option<Self> {
        let info = common::token_info::token_info(document_cache, token.clone())?;
        let name = i_slint_compiler::parser::normalize_identifier(token.text());

        let node = token.parent();

        fn property_parent(
            document_cache: &common::DocumentCache,
            node: &SyntaxNode,
        ) -> Option<TokenInformation> {
            let element = node.parent()?;
            assert_eq!(element.kind(), SyntaxKind::Element);
            let component_or_subelement = element.parent()?;
            assert!(
                [SyntaxKind::Component, SyntaxKind::SubElement]
                    .contains(&component_or_subelement.kind())
            );

            let declared_identifier =
                component_or_subelement.child_node(SyntaxKind::DeclaredIdentifier)?;

            let token = main_identifier(&declared_identifier)?;
            let info = common::token_info::token_info(document_cache, token.clone())?;
            let name = i_slint_compiler::parser::normalize_identifier(token.text());

            Some(TokenInformation { info, name, token })
        }

        let parent_node = node.parent()?;

        let parent_query = match parent_node.kind() {
            SyntaxKind::PropertyDeclaration => property_parent(document_cache, &parent_node),
            _ => None,
        };

        Some(DeclarationNodeQuery {
            token_info: TokenInformation { info, name, token },
            parent_token_info: parent_query,
        })
    }

    fn update_parent_token_info(&self, token: SyntaxToken) -> Option<Self> {
        let name = i_slint_compiler::parser::normalize_identifier(token.text());

        let mut query = self.token_info.clone();
        let mut parent_query = self.parent_token_info.clone();

        if let Some(pq) = &mut parent_query {
            pq.token = token;
            pq.name = name;
        } else {
            query.token = token;
            query.name = name;
        }

        Some(DeclarationNodeQuery { token_info: query, parent_token_info: parent_query })
    }

    fn is_export_identifier_or_external_name(&self) -> bool {
        self.token_info.token.kind() == SyntaxKind::Identifier
            && [SyntaxKind::ExportIdentifier, SyntaxKind::ExternalName]
                .contains(&self.token_info.token.parent().kind())
    }

    fn start_token(&self) -> Option<SyntaxToken> {
        if self.is_export_identifier_or_external_name() {
            None
        } else {
            Some(self.token_info.token.clone())
        }
    }

    /// Find the declaration node we should rename
    fn find_declaration_node(
        self,
        document_cache: &common::DocumentCache,
    ) -> Option<DeclarationNode> {
        let node = self.token_info.token.parent();

        match node.kind() {
            SyntaxKind::DeclaredIdentifier => Some(DeclarationNode {
                kind: DeclarationNodeKind::DeclaredIdentifier(node.into()),
                query: self,
            }),
            SyntaxKind::InternalName => Some(DeclarationNode {
                kind: DeclarationNodeKind::InternalName(node.into()),
                query: self,
            }),
            SyntaxKind::ExportName => Some(DeclarationNode {
                kind: DeclarationNodeKind::ExportName(node.into()),
                query: self,
            }),
            _ => {
                fn find_declared_identifier_in_element(
                    query: &DeclarationNodeQuery,
                    element: &syntax_nodes::Element,
                ) -> Option<syntax_nodes::DeclaredIdentifier> {
                    for prop in element.PropertyDeclaration() {
                        let identifier = prop.DeclaredIdentifier();
                        if i_slint_compiler::parser::identifier_text(&identifier).as_ref()
                            == Some(&query.token_info.name)
                        {
                            return Some(identifier);
                        }
                    }
                    for c in element.CallbackDeclaration() {
                        let identifier = c.DeclaredIdentifier();
                        if i_slint_compiler::parser::identifier_text(&identifier).as_ref()
                            == Some(&query.token_info.name)
                        {
                            return Some(identifier);
                        }
                    }
                    for f in element.Function() {
                        let identifier = f.DeclaredIdentifier();
                        if i_slint_compiler::parser::identifier_text(&identifier).as_ref()
                            == Some(&query.token_info.name)
                        {
                            return Some(identifier);
                        }
                    }

                    None
                }

                let declared_identifier = match &self.token_info.info {
                    common::token_info::TokenInfo::NamedReference(nr) => {
                        if i_slint_compiler::parser::normalize_identifier(nr.name())
                            == self.token_info.name
                        {
                            nr.element()
                                .borrow()
                                .debug
                                .iter()
                                .filter_map(|di| {
                                    find_declared_identifier_in_element(&self, &di.node)
                                })
                                .next()
                        } else {
                            None
                        }
                    }
                    common::token_info::TokenInfo::IncompleteNamedReference(element_type, name) => {
                        if name == &self.token_info.name {
                            match &element_type {
                                i_slint_compiler::langtype::ElementType::Component(component) => {
                                    find_declared_identifier_in_element(
                                        &self,
                                        &component.node.as_ref()?.Element(),
                                    )
                                }
                                _ => None,
                            }
                        } else {
                            None
                        }
                    }
                    _ => None,
                };
                if let Some(declared_identifier) = declared_identifier {
                    let token = main_identifier(&declared_identifier)?;

                    return Some(DeclarationNode {
                        query: DeclarationNodeQuery::new(document_cache, token)?,
                        kind: DeclarationNodeKind::DeclaredIdentifier(declared_identifier),
                    });
                }

                // Find the element/type manually so exports/imports get resolved
                let document = document_cache
                    .get_document_by_path(self.token_info.token.source_file.path())?;
                let document_node = document.node.clone()?;
                let start_token = self.start_token();

                find_declaration_node_impl(document_cache, &document_node, start_token, self)
            }
        }
    }

    fn has_parent_token_info(&self) -> bool {
        self.parent_token_info.is_some()
    }

    fn parent_token_info(&self) -> &TokenInformation {
        match &self.parent_token_info {
            Some(ti) => ti,
            None => &self.token_info,
        }
    }
}

fn find_declaration_node_impl(
    document_cache: &common::DocumentCache,
    document_node: &syntax_nodes::Document,
    start_token: Option<SyntaxToken>,
    query: DeclarationNodeQuery,
) -> Option<DeclarationNode> {
    let pti = query.parent_token_info();
    let ti = &query.token_info;

    // Exported under a custom name?
    if start_token.is_none() {
        for export_item in document_node.ExportsList() {
            if export_item.ExportModule().is_some() {
                continue;
            }

            for specifier in export_item.ExportSpecifier() {
                if let Some(export_name) = specifier.ExportName()
                    && i_slint_compiler::parser::identifier_text(&export_name).as_ref()
                        == Some(&pti.name)
                {
                    return Some(DeclarationNode {
                        kind: DeclarationNodeKind::ExportName(export_name),
                        query,
                    });
                }
            }
        }
    }

    let mut token = document_node.last_token();

    while let Some(t) = token {
        if let Some(declared_identifier) =
            find_last_declared_identifier_at_or_before(t.clone(), &ti.name)
        {
            if ti.is_same_symbol(document_cache, main_identifier(&declared_identifier).unwrap()) {
                return Some(DeclarationNode {
                    kind: DeclarationNodeKind::DeclaredIdentifier(declared_identifier.into()),
                    query,
                });
            }

            token = declared_identifier.first_token().and_then(|t| t.prev_token());
        } else {
            token = None;
        }
    }

    // Imported?
    let document_path = document_node.source_file.path();
    let document_dir = document_path.parent()?;

    for import_spec in document_node.ImportSpecifier() {
        if let Some(import_id) = import_spec.ImportIdentifierList() {
            for id in import_id.ImportIdentifier() {
                let external = i_slint_compiler::parser::identifier_text(&id.ExternalName());
                let internal =
                    id.InternalName().and_then(|i| i_slint_compiler::parser::identifier_text(&i));

                if internal.as_ref() == Some(&pti.name) {
                    return Some(DeclarationNode {
                        kind: DeclarationNodeKind::InternalName(id.InternalName().unwrap()),
                        query,
                    });
                }

                if external.as_ref() == Some(&pti.name) {
                    let path = import_path(document_dir, &import_spec)?;
                    let import_doc = document_cache.get_document_by_path(&path)?;
                    let import_doc_node = import_doc.node.as_ref()?;

                    return find_declaration_node_impl(
                        document_cache,
                        import_doc_node,
                        None,
                        query,
                    );
                }
            }
        }
    }

    // Find export modules!
    for export_item in document_node.ExportsList() {
        let Some(module) = export_item.ExportModule() else {
            continue;
        };

        let path = import_path(document_dir, &module)?;
        let import_doc = document_cache.get_document_by_path(&path)?;
        let import_doc_node = import_doc.node.as_ref()?;

        if module.child_token(SyntaxKind::Star).is_some() {
            if let Some(declaration_node) =
                find_declaration_node_impl(document_cache, import_doc_node, None, query.clone())
            {
                return Some(declaration_node);
            } else {
                continue;
            }
        } else {
            for specifier in export_item.ExportSpecifier() {
                if let Some(export_name) = specifier.ExportName()
                    && i_slint_compiler::parser::identifier_text(&export_name).as_ref()
                        == Some(&pti.name)
                {
                    return Some(DeclarationNode {
                        kind: DeclarationNodeKind::ExportName(export_name),
                        query,
                    });
                }

                let identifier =
                    i_slint_compiler::parser::identifier_text(&specifier.ExportIdentifier());

                if identifier.as_ref() == Some(&pti.name) {
                    return find_declaration_node_impl(
                        document_cache,
                        import_doc_node,
                        None,
                        query,
                    );
                }
            }
        }
    }

    None
}

/// Rename a `DeclaredIdentifier`.
///
/// This is a locally defined thing.
///
/// Fix up local usages, fix exports and any imports elsewhere if the exports changed
fn rename_declared_identifier(
    document_cache: &common::DocumentCache,
    query: &DeclarationNodeQuery,
    declared_identifier: &syntax_nodes::DeclaredIdentifier,
    new_type: &str,
) -> crate::Result<lsp_types::WorkspaceEdit> {
    let ti = &query.token_info;

    let source_file = &declared_identifier.source_file;
    let document = document_cache
        .get_document_for_source_file(source_file)
        .expect("Identifier is in unknown document");

    let Some(document_node) = &document.node else {
        return Err("No document found".into());
    };

    let parent = declared_identifier.parent().unwrap();

    let normalized_new_type = i_slint_compiler::parser::normalize_identifier(new_type);

    if parent.kind() != SyntaxKind::Component
        && document.local_registry.lookup(normalized_new_type.as_str())
            != i_slint_compiler::langtype::Type::Invalid
    {
        return Err(format!("{new_type} is already a registered type").into());
    }
    if parent.kind() == SyntaxKind::Component
        && document.local_registry.lookup_element(normalized_new_type.as_str()).is_ok()
    {
        return Err(format!("{new_type} is already a registered element").into());
    }

    let old_type = &ti.name;

    if *old_type == normalized_new_type {
        return Ok(lsp_types::WorkspaceEdit::default());
    }

    let mut edits = Vec::new();

    // Change all local usages:
    rename_local_symbols(document_cache, document_node, query, new_type, &mut edits);

    if is_symbol_name_exported(document_cache, document_node, query).is_some()
        && fix_export_lists(document_cache, document_node, query, new_type, &mut edits).is_none()
    {
        fix_imports(document_cache, query, source_file.path(), new_type, &mut edits);
    }

    Ok(common::create_workspace_edit_from_single_text_edits(edits))
}

#[cfg(test)]
mod tests {
    use lsp_types::Url;

    use super::*;

    use std::collections::HashMap;

    use crate::common::test;
    use crate::common::text_edit;

    #[track_caller]
    fn find_token_by_comment(
        document_cache: &common::DocumentCache,
        document_path: &Path,
        suffix: &str,
    ) -> SyntaxToken {
        let document = document_cache.get_document_by_path(document_path).unwrap();
        let document = document.node.as_ref().unwrap();

        let offset =
            document.text().to_string().find(&format!("<- TEST_ME{suffix} ")).unwrap() as u32;
        let comment = document.token_at_offset(offset.into()).next().unwrap();
        assert_eq!(comment.kind(), SyntaxKind::Comment);
        let mut token = comment.prev_token();

        while let Some(t) = &token {
            if ![SyntaxKind::Comment, SyntaxKind::Eof, SyntaxKind::Whitespace].contains(&t.kind()) {
                break;
            }
            token = t.prev_token();
        }
        token.unwrap()
    }

    #[track_caller]
    fn find_node_by_comment(
        document_cache: &common::DocumentCache,
        document_path: &Path,
        suffix: &str,
    ) -> SyntaxNode {
        find_token_by_comment(document_cache, document_path, suffix).parent()
    }

    #[track_caller]
    fn apply_text_changes(
        document_cache: &common::DocumentCache,
        edit: &lsp_types::WorkspaceEdit,
    ) -> Vec<text_edit::EditedText> {
        tracing::debug!("Edit:");
        for it in text_edit::EditIterator::new(edit) {
            tracing::debug!("   {} => {:?}", it.0.uri, it.1);
        }
        tracing::debug!("*** All edits reported ***");

        let changed_text = text_edit::apply_workspace_edit(document_cache, edit).unwrap();
        assert!(!changed_text.is_empty()); // there was a change!

        tracing::debug!("After changes were applied:");
        for ct in &changed_text {
            tracing::debug!("File {}:", ct.url);
            for (count, line) in ct.contents.split('\n').enumerate() {
                tracing::debug!("    {:3}: {line}", count + 1);
            }
            tracing::debug!("=========");
        }
        tracing::debug!("*** All changes reported ***");

        changed_text
    }

    #[track_caller]
    fn compile_test_changes(
        document_cache: &common::DocumentCache,
        edit: &lsp_types::WorkspaceEdit,
        allow_warnings: bool,
    ) -> Vec<text_edit::EditedText> {
        let changed_text = apply_text_changes(document_cache, edit);
        let changed_text_count = changed_text.len();
        let code = {
            let mut map: HashMap<Url, String> = document_cache
                .all_url_documents()
                .map(|(url, dn)| (url, dn.source_file.as_ref()))
                .map(|(url, sf)| (url, sf.source().unwrap().to_string()))
                .collect();
            for ct in &changed_text {
                map.insert(ct.url.clone(), ct.contents.clone());
            }
            map
        };

        // changed code compiles fine:
        let new_document_cache = test::recompile_test_with_sources("fluent", code, allow_warnings);

        // try to apply the reverse change. That should lead to the same result
        let reversed_edit = text_edit::reversed_edit(document_cache, edit).unwrap();
        let reversed_edits = apply_text_changes(&new_document_cache, &reversed_edit);
        assert_eq!(changed_text_count, reversed_edits.len());
        for e in reversed_edits {
            let doc = document_cache.get_document(&e.url).unwrap();
            assert_eq!(doc.node.as_ref().unwrap().source_file.source().unwrap(), e.contents);
        }

        changed_text
    }

    #[track_caller]
    pub fn rename_tester_with_new_name(
        document_cache: &common::DocumentCache,
        document_path: &Path,
        suffix: &str,
        new_name: &str,
    ) -> Vec<text_edit::EditedText> {
        let edit = find_declaration_node(
            document_cache,
            &find_token_by_comment(document_cache, document_path, suffix),
        )
        .expect("Node not found")
        .rename(document_cache, new_name)
        .expect("Rename failed");
        compile_test_changes(document_cache, &edit, false)
    }

    #[track_caller]
    pub fn rename_tester(
        document_cache: &common::DocumentCache,
        document_path: &Path,
        suffix: &str,
    ) -> Vec<text_edit::EditedText> {
        rename_tester_with_new_name(document_cache, document_path, suffix, "XxxYyyZzz")
    }

    #[test]
    fn test_host_language_classification() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
export component Foo inherits Window {
    in property <int> count /* <- TEST_ME_PROP */;
    private property <string> secret /* <- TEST_ME_PRIV */;
    callback clicked /* <- TEST_ME_CB */();
    public function multiply /* <- TEST_ME_FN */(x: int) -> int { x * 2 }
}

component Inner {
    in property <int> hidden /* <- TEST_ME_INNER */;
}
                "#
                .to_string(),
            )]),
            true,
        );

        let classify = |suffix: &str| -> Option<HostLanguageRenameInfo> {
            let token =
                find_token_by_comment(&document_cache, &test::main_test_file_name(), suffix);
            find_declaration_node(&document_cache, &token)?
                .host_language_classification(&document_cache)
        };

        let prop = classify("_PROP").expect("public property should classify");
        assert_eq!(prop.kind, DeclarationKind::Property);
        assert_eq!(prop.old_name, "count");

        assert!(classify("_PRIV").is_none(), "private property must not classify");

        let cb = classify("_CB").expect("public callback should classify");
        assert_eq!(cb.kind, DeclarationKind::Callback);
        assert_eq!(cb.old_name, "clicked");

        let func = classify("_FN").expect("public function should classify");
        assert_eq!(func.kind, DeclarationKind::Function);
        assert_eq!(func.old_name, "multiply");

        assert!(
            classify("_INNER").is_none(),
            "property on non-exported inner component must not classify",
        );
    }

    #[test]
    fn test_host_language_classification_global_property() {
        // Globals also emit Rust/C++ public accessors (via global<T>()).
        // Their declarations must classify just like component-root
        // declarations.
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
export global Settings {
    in-out property <int> volume /* <- TEST_ME_GLOBAL_PROP */;
    callback changed /* <- TEST_ME_GLOBAL_CB */;
}

export component Host inherits Window { }
                "#
                .to_string(),
            )]),
            true,
        );

        let classify = |suffix: &str| -> Option<HostLanguageRenameInfo> {
            let token =
                find_token_by_comment(&document_cache, &test::main_test_file_name(), suffix);
            find_declaration_node(&document_cache, &token)?
                .host_language_classification(&document_cache)
        };

        let prop = classify("_GLOBAL_PROP").expect("public global property should classify");
        assert_eq!(prop.kind, DeclarationKind::Property);
        assert_eq!(prop.old_name, "volume");

        let cb = classify("_GLOBAL_CB").expect("public global callback should classify");
        assert_eq!(cb.kind, DeclarationKind::Callback);
    }

    #[test]
    fn test_host_language_classification_cross_file_inherited_property() {
        // Property declared in a non-exported base in file A, inherited by
        // an exported component in file B. The classifier must walk all
        // documents (not just the renamed file's own document) to find the
        // exported descendant.
        let base_url = Url::from_file_path(test::test_file_name("base.slint")).unwrap();
        let app_url = Url::from_file_path(test::main_test_file_name()).unwrap();
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    base_url.clone(),
                    r#"
export component Base {
    in property <int> base-count /* <- TEST_ME_CROSSFILE */;
}
                    "#
                    .to_string(),
                ),
                (
                    app_url.clone(),
                    format!(
                        r#"
import {{ Base }} from "{}";
export component App inherits Base {{ }}
                        "#,
                        "base.slint",
                    ),
                ),
            ]),
            true,
        );

        let base_path = test::test_file_name("base.slint");
        let token = find_token_by_comment(&document_cache, &base_path, "_CROSSFILE");
        let info = find_declaration_node(&document_cache, &token)
            .and_then(|n| n.host_language_classification(&document_cache))
            .expect("inherited property exposed via cross-file export must classify");
        assert_eq!(info.kind, DeclarationKind::Property);
        assert_eq!(info.old_name, "base-count");
    }

    #[test]
    fn test_host_language_classification_inherited_property() {
        // A property declared on a non-exported base but exposed by an
        // exported component via `inherits` must still classify -- the
        // codegen DOES generate the accessor on the exported component.
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
component Base {
    in property <int> base-count /* <- TEST_ME_INHERITED */;
}

export component App inherits Window {
    base := Base { }
}

export component DerivedApp inherits Base {
    in property <int> own-prop /* <- TEST_ME_OWN */;
}
                "#
                .to_string(),
            )]),
            true,
        );

        let classify = |suffix: &str| -> Option<HostLanguageRenameInfo> {
            let token =
                find_token_by_comment(&document_cache, &test::main_test_file_name(), suffix);
            find_declaration_node(&document_cache, &token)?
                .host_language_classification(&document_cache)
        };

        let inherited = classify("_INHERITED")
            .expect("property inherited into an exported component must classify");
        assert_eq!(inherited.kind, DeclarationKind::Property);
        assert_eq!(inherited.old_name, "base-count");

        let own = classify("_OWN").expect("locally-declared property on exported component");
        assert_eq!(own.old_name, "own-prop");
    }

    #[test]
    fn test_rename_redefined_component() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
component Foo /* <- TEST_ME_1 */ { @children }

export { Foo /* 2 */ }

component Bar {
    Foo /* 1.1 */ { }
}

component Foo /* <- TEST_ME_2 */ inherits Foo /* 1.2 */ {
    Foo /* 1.3 */ { }
}
                "#
                .to_string(),
            )]),
            true, // Component `Foo` is replacing a component with the same name
        );

        // Can not rename the first one...
        assert!(
            find_declaration_node(
                &document_cache,
                &find_token_by_comment(&document_cache, &test::main_test_file_name(), "_1"),
            )
            .is_none(),
        );

        let edit = find_declaration_node(
            &document_cache,
            &find_token_by_comment(&document_cache, &test::main_test_file_name(), "_2"),
        )
        .unwrap()
        .rename(&document_cache, "XxxYyyZzz")
        .unwrap();

        let edited_text = apply_text_changes(&document_cache, &edit); // DO NOT COMPILE, THAT WILL FAIL!

        assert_eq!(edited_text.len(), 1);

        assert!(edited_text[0].contents.contains("component Foo /* <- TEST_ME_1 "));
        // The *last* Foo gets exported
        assert!(edited_text[0].contents.contains("export { XxxYyyZzz /* 2 */ }"));

        // All the following are wrong:
        assert!(edited_text[0].contents.contains("XxxYyyZzz /* 1.1 "));
        assert!(edited_text[0].contents.contains("inherits XxxYyyZzz /* 1.2 "));
        assert!(edited_text[0].contents.contains("XxxYyyZzz /* 1.3 "));
    }

    #[test]
    fn test_rename_redefined_enum() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
enum Foo /* <- TEST_ME_1 */ { test1 }

export { Foo /* 2 */ }

struct Bar {
    bar_test: Foo
}

enum Foo /* <- TEST_ME_2 */ {
    test2
}

export struct Baz {
    baz_test: Foo
}
                "#
                .to_string(),
            )]),
            true, // Component `Foo` is replacing a component with the same name
        );

        // Can not rename the first one...
        assert!(
            find_declaration_node(
                &document_cache,
                &find_token_by_comment(&document_cache, &test::main_test_file_name(), "_1"),
            )
            .is_none(),
        );

        let edit = find_declaration_node(
            &document_cache,
            &find_token_by_comment(&document_cache, &test::main_test_file_name(), "_2"),
        )
        .unwrap()
        .rename(&document_cache, "XxxYyyZzz")
        .unwrap();

        let edited_text = apply_text_changes(&document_cache, &edit); // DO NOT COMPILE, THAT WILL FAIL!

        assert_eq!(edited_text.len(), 1);

        assert!(edited_text[0].contents.contains("enum Foo /* <- TEST_ME_1 "));
        // The *last* Foo gets exported!
        assert!(edited_text[0].contents.contains("export { XxxYyyZzz /* 2 */ }"));
        assert!(edited_text[0].contents.contains("baz_test: XxxYyyZzz"));

        // All the following are wrong:
        assert!(edited_text[0].contents.contains("bar_test: XxxYyyZzz"));
    }

    #[test]
    fn test_rename_redefined_struct() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
struct Foo /* <- TEST_ME_1 */ { test: bool }

export { Foo /* 2 */ }

struct Bar {
    bar_test: Foo
}

struct Foo /* <- TEST_ME_2 */ {
    foo_test: Foo
}
                "#
                .to_string(),
            )]),
            true, // Component `Foo` is replacing a component with the same name
        );

        // Can not rename the first one...
        assert!(
            find_declaration_node(
                &document_cache,
                &find_token_by_comment(&document_cache, &test::main_test_file_name(), "_1"),
            )
            .is_none(),
        );

        let edit = find_declaration_node(
            &document_cache,
            &find_token_by_comment(&document_cache, &test::main_test_file_name(), "_2"),
        )
        .unwrap()
        .rename(&document_cache, "XxxYyyZzz")
        .unwrap();

        let edited_text = apply_text_changes(&document_cache, &edit); // DO NOT COMPILE, THAT WILL FAIL!

        assert_eq!(edited_text.len(), 1);

        assert!(edited_text[0].contents.contains("struct Foo /* <- TEST_ME_1 "));
        // The *last* Foo gets exported!
        assert!(edited_text[0].contents.contains("export { XxxYyyZzz /* 2 */ }"));

        // All the following are wrong:
        assert!(edited_text[0].contents.contains("bar_test: XxxYyyZzz"));
        assert!(edited_text[0].contents.contains("foo_test: XxxYyyZzz"));
    }

    #[test]
    fn test_rename_component_from_definition() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
export { Foo }

enum Xyz { Foo, Bar }

struct Abc { Foo: Xyz }

component Foo /* <- TEST_ME_1 */ inherits Rectangle {
    @children
}

component Baz {
    Foo /* <- TEST_ME_2 */ { }
}

struct Foo {
    bar: bool,
}

export component Bar inherits Foo /* <- TEST_ME_3 */ {
    Foo /* <- TEST_ME_4 */ { }
    Rectangle {
        Foo /* <- TEST_ME_5 */ { }
        Foo := Baz { }
    }

    if true: Rectangle {
        Foo /* <- TEST_ME_6 */ { }
    }

    if false: Rectangle {
        Foo /* <- TEST_ME_7 */ { }
    }

    function Foo(Foo: int) { Foo + 1; }
    function F() { self.Foo(42); }

    for i in [1, 2, 3]: Foo /* <- TEST_ME_8 */ { }
}
                "#
                .to_string(),
            )]),
            true, // Component `Foo` is replacing a component with the same name
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");
        assert_eq!(edited_text.len(), 1);

        assert_eq!(edited_text.len(), 1);
        assert!(edited_text[0].contents.contains("export { XxxYyyZzz }"));
        assert!(edited_text[0].contents.contains("enum Xyz { Foo,"));
        assert!(edited_text[0].contents.contains("struct Abc { Foo:"));
        assert!(edited_text[0].contents.contains("component XxxYyyZzz /* <- TEST_ME_1 "));
        assert!(edited_text[0].contents.contains("TEST_ME_1 */ inherits Rectangle "));
        assert!(edited_text[0].contents.contains("XxxYyyZzz /* <- TEST_ME_2 "));
        assert!(edited_text[0].contents.contains("struct Foo {"));
        assert!(
            edited_text[0].contents.contains("component Bar inherits XxxYyyZzz /* <- TEST_ME_3 ")
        );
        assert!(edited_text[0].contents.contains("XxxYyyZzz /* <- TEST_ME_4 "));
        assert!(edited_text[0].contents.contains("XxxYyyZzz /* <- TEST_ME_5 "));
        assert!(edited_text[0].contents.contains("Foo := Baz "));
        assert!(edited_text[0].contents.contains("XxxYyyZzz /* <- TEST_ME_6 "));
        assert!(edited_text[0].contents.contains("XxxYyyZzz /* <- TEST_ME_7 "));
        assert!(edited_text[0].contents.contains("function Foo(Foo:"));
        assert!(edited_text[0].contents.contains("F() { self.Foo("));
        assert!(edited_text[0].contents.contains("XxxYyyZzz /* <- TEST_ME_8 "));
    }

    #[test]
    fn test_rename_component_from_definition_live_preview_rename() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                "component Foo/* <- TEST_ME_1 */{ }\nexport component _SLINT_LivePreview inherits Foo { /* @lsp:ignore-node */ }\n".to_string()
            )]),
            true,
        );

        let edit = find_declaration_node(
            &document_cache,
            &find_token_by_comment(&document_cache, &test::main_test_file_name(), "_1"),
        )
        .unwrap()
        .rename(&document_cache, "XxxYyyZzz")
        .unwrap();

        assert_eq!(text_edit::EditIterator::new(&edit).count(), 1);

        // This does not compile as the type was not changed in the _SLINT_LivePreview part.
        // This is intended as that code does not really exist in the first place!
    }

    #[test]
    fn test_rename_component_from_definition_with_renaming_export() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { FExport} from "source.slint";

export component Foo {
    FExport { }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
enum Foo {
    foo, bar
}

component Foo /* <- TEST_ME_1 */ {
    property <Foo> test-property: Foo.bar;
}

export { Foo as FExport }
                "#
                    .to_string(),
                ),
            ]),
            false,
        );

        let edited_text =
            rename_tester(&document_cache, &test::test_file_name("source.slint"), "_1");

        assert_eq!(edited_text.len(), 1);
        assert_eq!(
            edited_text[0].url.to_file_path().unwrap(),
            test::test_file_name("source.slint")
        );
        assert!(edited_text[0].contents.contains("enum Foo {"));
        assert!(edited_text[0].contents.contains("component XxxYyyZzz /* <- TEST_ME_1 "));
        assert!(edited_text[0].contents.contains("property <Foo> test-property"));
        assert!(edited_text[0].contents.contains("test-property: Foo.bar;"));
        assert!(edited_text[0].contents.contains("export { XxxYyyZzz as FExport }"));
    }

    #[test]
    fn test_rename_component_from_definition_with_export() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { Foo } from "source.slint";
import { UserComponent } from "user.slint";
import { User2Component } from "user2.slint";
import { Foo as User3Fxx } from "user3.slint";
import { User4Fxx } from "user4.slint";

export component Main {
    Foo { }
    UserComponent { }
    User2Component { }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
export component Foo /* <- TEST_ME_1 */ { }
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user.slint")).unwrap(),
                    r#"
import { Foo as Bar } from "source.slint";

export component UserComponent {
    Bar { }
}

export { Bar }
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user2.slint")).unwrap(),
                    r#"
import { Foo as XxxYyyZzz } from "source.slint";

export component User2Component {
    XxxYyyZzz { }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user3.slint")).unwrap(),
                    r#"
import { Foo } from "source.slint";

export { Foo }
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user4.slint")).unwrap(),
                    r#"
import { Foo } from "source.slint";

export { Foo as User4Fxx }
                "#
                    .to_string(),
                ),
            ]),
            false,
        );

        let edited_text =
            rename_tester(&document_cache, &test::test_file_name("source.slint"), "_1");

        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains("XxxYyyZzz"));
                assert!(!ed.contents.contains("Foo"));
                assert!(ed.contents.contains("UserComponent"));
                assert!(ed.contents.contains("import { XxxYyyZzz as User3Fxx }"));
                assert!(ed.contents.contains("import { User4Fxx }"));
            } else if ed_path == test::test_file_name("source.slint") {
                assert!(ed.contents.contains("export component XxxYyyZzz /* <- TEST_ME_1 "));
                assert!(!ed.contents.contains("Foo"));
            } else if ed_path == test::test_file_name("user.slint") {
                assert!(ed.contents.contains("{ XxxYyyZzz as Bar }"));
                assert!(ed.contents.contains("Bar { }"));
                assert!(!ed.contents.contains("Foo"));
            } else if ed_path == test::test_file_name("user2.slint") {
                assert!(ed.contents.contains("import { XxxYyyZzz }"));
                assert!(ed.contents.contains("XxxYyyZzz { }"));
            } else if ed_path == test::test_file_name("user3.slint") {
                assert!(ed.contents.contains("import { XxxYyyZzz }"));
                assert!(ed.contents.contains("export { XxxYyyZzz }"));
            } else if ed_path == test::test_file_name("user4.slint") {
                assert!(ed.contents.contains("import { XxxYyyZzz }"));
                assert!(ed.contents.contains("export { XxxYyyZzz as User4Fxx }"));
            } else {
                unreachable!();
            }
        }
    }

    #[test]
    fn test_rename_component_from_definition_with_export_and_relative_paths() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { Foo } from "s/source.slint";
import { UserComponent } from "u/user.slint";
import { User2Component } from "u/user2.slint";
import { Foo as User3Fxx } from "u/user3.slint";
import { User4Fxx } from "u/user4.slint";

export component Main {
    Foo { }
    UserComponent { }
    User2Component { }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("s/source.slint")).unwrap(),
                    r#"
export component Foo /* <- TEST_ME_1 */ { }
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("u/user.slint")).unwrap(),
                    r#"
import { Foo as Bar } from "../s/source.slint";

export component UserComponent {
    Bar { }
}

export { Bar }
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("u/user2.slint")).unwrap(),
                    r#"
import { Foo as XxxYyyZzz } from "../s/source.slint";

export component User2Component {
    XxxYyyZzz { }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("u/user3.slint")).unwrap(),
                    r#"
import { Foo } from "../s/source.slint";

export { Foo }
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("u/user4.slint")).unwrap(),
                    r#"
import { Foo } from "../s/source.slint";

export { Foo as User4Fxx }
                "#
                    .to_string(),
                ),
            ]),
            false,
        );

        let edited_text =
            rename_tester(&document_cache, &test::test_file_name("s/source.slint"), "_1");

        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains("XxxYyyZzz"));
                assert!(!ed.contents.contains("Foo"));
                assert!(ed.contents.contains("UserComponent"));
                assert!(ed.contents.contains("import { XxxYyyZzz as User3Fxx }"));
                assert!(ed.contents.contains("import { User4Fxx }"));
            } else if ed_path == test::test_file_name("s/source.slint") {
                assert!(ed.contents.contains("export component XxxYyyZzz /* <- TEST_ME_1 "));
                assert!(!ed.contents.contains("Foo"));
            } else if ed_path == test::test_file_name("u/user.slint") {
                assert!(ed.contents.contains("{ XxxYyyZzz as Bar }"));
                assert!(ed.contents.contains("Bar { }"));
                assert!(!ed.contents.contains("Foo"));
            } else if ed_path == test::test_file_name("u/user2.slint") {
                assert!(ed.contents.contains("import { XxxYyyZzz }"));
                assert!(ed.contents.contains("XxxYyyZzz { }"));
            } else if ed_path == test::test_file_name("u/user3.slint") {
                assert!(ed.contents.contains("import { XxxYyyZzz }"));
                assert!(ed.contents.contains("export { XxxYyyZzz }"));
            } else if ed_path == test::test_file_name("u/user4.slint") {
                assert!(ed.contents.contains("import { XxxYyyZzz }"));
                assert!(ed.contents.contains("export { XxxYyyZzz as User4Fxx }"));
            } else {
                unreachable!();
            }
        }
    }

    #[test]
    fn test_rename_component_from_definition_import_confusion() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { Foo as User1Fxx } from "user1.slint";
import { Foo as User2Fxx } from "user2.slint";

export component Main {
    User1Fxx { }
    User2Fxx { }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user1.slint")).unwrap(),
                    r#"
export component Foo /* <- TEST_ME_1 */ { }
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user2.slint")).unwrap(),
                    r#"
export component Foo /* <- TEST_ME_2 */ { }
                "#
                    .to_string(),
                ),
            ]),
            false,
        );

        let edited_text =
            rename_tester(&document_cache, &test::test_file_name("user1.slint"), "_1");

        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains("import { XxxYyyZzz as User1Fxx }"));
                assert!(ed.contents.contains("import { Foo as User2Fxx }"));
            } else if ed_path == test::test_file_name("user1.slint") {
                assert!(ed.contents.contains("export component XxxYyyZzz /* <- TEST_ME_1 "));
                assert!(!ed.contents.contains("Foo"));
            } else {
                unreachable!();
            }
        }

        let edited_text =
            rename_tester(&document_cache, &test::test_file_name("user2.slint"), "_2");

        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains("import { XxxYyyZzz as User2Fxx }"));
                assert!(ed.contents.contains("import { Foo as User1Fxx }"));
            } else if ed_path == test::test_file_name("user2.slint") {
                assert!(ed.contents.contains("export component XxxYyyZzz /* <- TEST_ME_2 "));
                assert!(!ed.contents.contains("Foo"));
            } else {
                unreachable!();
            }
        }
    }

    #[test]
    fn test_rename_component_from_definition_redefinition_error() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
struct UsedStruct { value: int, }
enum UsedEnum { x, y }

component Foo /* <- TEST_ME_1 */ { }

component Baz {
    Foo { }
}

export component Bar {
    Foo { }
    Rectangle {
        Foo { }
        Baz { }
    }
}
                    "#
                .to_string(),
            )]),
            false,
        );

        let dn = find_declaration_node(
            &document_cache,
            &find_token_by_comment(&document_cache, &test::main_test_file_name(), "_1"),
        )
        .unwrap();

        assert!(dn.rename(&document_cache, "Foo").is_err());
        assert!(dn.rename(&document_cache, "UsedStruct").is_ok());
        assert!(dn.rename(&document_cache, "UsedEnum").is_ok());
        assert!(dn.rename(&document_cache, "Baz").is_err());
        assert!(dn.rename(&document_cache, "HorizontalLayout").is_err());
    }

    #[test]
    fn test_rename_struct_from_definition() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
export struct Foo /* <- TEST_ME_1 */ {
    test-me: bool,
}

component Baz {
    in-out property <Foo> baz-prop;
}

export component Bar {
    in-out property <Foo> bar-prop <=> baz.baz-prop;

    baz := Baz {}
}
                    "#
                .to_string(),
            )]),
            false,
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");

        assert_eq!(edited_text.len(), 1);
        assert!(edited_text[0].contents.contains("struct XxxYyyZzz /* <- TEST_ME_1 "));
        assert!(edited_text[0].contents.contains("property <XxxYyyZzz> baz-prop"));
        assert!(edited_text[0].contents.contains("property <XxxYyyZzz> bar-prop"));
    }

    #[test]
    fn test_rename_struct_from_definition_with_dash() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
export struct F-oo /* <- TEST_ME_1 */ {
    test-me: bool,
}

component Baz {
    in-out property <F_oo> baz-prop;
}

export component Bar {
    in-out property <F-oo> bar-prop <=> baz.baz-prop;

    baz := Baz {}
}
                    "#
                .to_string(),
            )]),
            false,
        );

        let edited_text = rename_tester_with_new_name(
            &document_cache,
            &test::main_test_file_name(),
            "_1",
            "Xxx_Yyy-Zzz",
        );

        assert_eq!(edited_text.len(), 1);
        assert!(edited_text[0].contents.contains("export struct Xxx_Yyy-Zzz /* <- TEST_ME_1 "));
        assert!(edited_text[0].contents.contains("<Xxx_Yyy-Zzz> baz-prop"));
        assert!(edited_text[0].contents.contains("<Xxx_Yyy-Zzz> bar-prop"));
    }

    #[test]
    fn test_rename_struct_from_definition_with_underscore() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
export struct F_oo /* <- TEST_ME_1 */ {
    test-me: bool,
}

component Baz {
    in-out property <F_oo> baz-prop;
}

export component Bar {
    in-out property <F-oo> bar-prop <=> baz.baz-prop;

    baz := Baz {}
}
                    "#
                .to_string(),
            )]),
            false,
        );

        let edited_text = rename_tester_with_new_name(
            &document_cache,
            &test::main_test_file_name(),
            "_1",
            "Xxx_Yyy-Zzz",
        );

        assert_eq!(edited_text.len(), 1);
        assert!(edited_text[0].contents.contains("export struct Xxx_Yyy-Zzz /* <- TEST_ME_1 "));
        assert!(edited_text[0].contents.contains("<Xxx_Yyy-Zzz> baz-prop"));
        assert!(edited_text[0].contents.contains("<Xxx_Yyy-Zzz> bar-prop"));
    }

    #[test]
    fn test_rename_struct_from_definition_with_renaming_export() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { FExport} from "source.slint";

export component Foo {
    property <FExport> foo-prop;
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
struct Foo /* <- TEST_ME_1 */ {
    test-me: bool,
}

export { Foo as FExport }
                "#
                    .to_string(),
                ),
            ]),
            false,
        );

        let edited_text =
            rename_tester(&document_cache, &test::test_file_name("source.slint"), "_1");

        assert_eq!(edited_text.len(), 1);
        assert_eq!(
            edited_text[0].url.to_file_path().unwrap(),
            test::test_file_name("source.slint")
        );
        assert!(edited_text[0].contents.contains("struct XxxYyyZzz /* <- TEST_ME_1 "));
        assert!(edited_text[0].contents.contains("export { XxxYyyZzz as FExport }"));
    }

    #[test]
    fn test_rename_struct_from_definition_with_export() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { Foo } from "source.slint";
import { UserComponent } from "user.slint";
import { User2Struct } from "user2.slint";
import { Foo as User3Fxx } from "user3.slint";
import { User4Fxx } from "user4.slint";

export component Main {
    property <Foo> main-prop;
    property <User3Fxx> main-prop2;
    property <User2Struct> main-prop3;
    property <User3Fxx> main-prop4 <=> uc.user-component-prop;

    uc := UserComponent { }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
export struct Foo /* <- TEST_ME_1 */ { test-me: bool, }
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user.slint")).unwrap(),
                    r#"
import { Foo as Bar } from "source.slint";

export component UserComponent {
    in-out property <Bar> user-component-prop;
}

export { Bar }
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user2.slint")).unwrap(),
                    r#"
import { Foo as XxxYyyZzz } from "source.slint";

export struct User2Struct {
    member: XxxYyyZzz,
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user3.slint")).unwrap(),
                    r#"
import { Foo } from "source.slint";

export { Foo }
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user4.slint")).unwrap(),
                    r#"
import { Foo } from "source.slint";

export { Foo as User4Fxx }
                "#
                    .to_string(),
                ),
            ]),
            false,
        );

        let edited_text =
            rename_tester(&document_cache, &test::test_file_name("source.slint"), "_1");

        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains("XxxYyyZzz"));
                assert!(!ed.contents.contains("Foo"));
                assert!(ed.contents.contains("UserComponent"));
                assert!(ed.contents.contains("import { XxxYyyZzz as User3Fxx }"));
                assert!(ed.contents.contains("import { User4Fxx }"));
            } else if ed_path == test::test_file_name("source.slint") {
                assert!(ed.contents.contains("export struct XxxYyyZzz /* <- TEST_ME_1 "));
                assert!(!ed.contents.contains("Foo"));
            } else if ed_path == test::test_file_name("user.slint") {
                assert!(ed.contents.contains("{ XxxYyyZzz as Bar }"));
                assert!(ed.contents.contains("property <Bar> user-component-prop"));
                assert!(!ed.contents.contains("Foo"));
            } else if ed_path == test::test_file_name("user2.slint") {
                assert!(ed.contents.contains("import { XxxYyyZzz }"));
                assert!(ed.contents.contains("member: XxxYyyZzz,"));
            } else if ed_path == test::test_file_name("user3.slint") {
                assert!(ed.contents.contains("import { XxxYyyZzz }"));
                assert!(ed.contents.contains("export { XxxYyyZzz }"));
            } else if ed_path == test::test_file_name("user4.slint") {
                assert!(ed.contents.contains("import { XxxYyyZzz }"));
                assert!(ed.contents.contains("export { XxxYyyZzz as User4Fxx }"));
            } else {
                unreachable!();
            }
        }
    }

    #[test]
    fn test_rename_enum_from_definition() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
    export { Foo }

    enum Foo /* <- TEST_ME_1 */ {
        test,
    }

    component Baz {
        in-out property <Foo> baz-prop: Foo.test;
    }

    export component Bar {
        in-out property <Foo> bar-prop <=> baz.baz-prop;

        baz := Baz {}
    }
                        "#
                .to_string(),
            )]),
            true, // redefinition of type warning
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");

        assert_eq!(edited_text.len(), 1);
        assert!(edited_text[0].contents.contains("export { XxxYyyZzz }"));
        assert!(edited_text[0].contents.contains("enum XxxYyyZzz /* <- TEST_ME_1 "));
        assert!(edited_text[0].contents.contains("test,"));
        assert!(edited_text[0].contents.contains("property <XxxYyyZzz> baz-prop"));
        assert!(edited_text[0].contents.contains("baz-prop: XxxYyyZzz.test;"));
        assert!(edited_text[0].contents.contains("property <XxxYyyZzz> bar-prop"));
    }

    #[test]
    fn test_rename_enum_from_definition_with_struct() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
    enum Foo /* <- TEST_ME_1 */ {
        M1, M2,
    }

    export { Foo }

    component Baz {
        in-out property <Foo> baz-prop;
    }

    export component Bar {
        in-out property <Foo> bar-prop <=> baz.baz-prop;

        baz := Baz {}
    }
                        "#
                .to_string(),
            )]),
            true, // redefinition of type warning
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");

        assert_eq!(edited_text.len(), 1);
        assert!(edited_text[0].contents.contains("enum XxxYyyZzz /* <- TEST_ME_1 */ "));
        assert!(edited_text[0].contents.contains("export { XxxYyyZzz }"));
        assert!(edited_text[0].contents.contains("property <XxxYyyZzz> baz-prop"));
        assert!(edited_text[0].contents.contains("property <XxxYyyZzz> bar-prop"));
    }

    #[test]
    fn test_rename_enum_from_definition_with_renaming_export() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
    import { FExport} from "source.slint";

    export enum Foo {
        M1, M2
    }
                    "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
    export enum Foo /* <- TEST_ME_1 */ {
        OM1, OM2,
    }

    export { Foo as FExport }
                    "#
                    .to_string(),
                ),
            ]),
            false,
        );

        let edited_text =
            rename_tester(&document_cache, &test::test_file_name("source.slint"), "_1");

        assert_eq!(edited_text.len(), 1);
        assert_eq!(
            edited_text[0].url.to_file_path().unwrap(),
            test::test_file_name("source.slint")
        );
        assert!(edited_text[0].contents.contains("export enum XxxYyyZzz /* <- TEST_ME_1 "));
        assert!(edited_text[0].contents.contains("export { XxxYyyZzz as FExport"));
        assert!(!edited_text[0].contents.contains("Foo"));
    }

    #[test]
    fn test_rename_enum_from_definition_with_export() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
    import { F_o_o } from "source.slint";
    import { UserComponent } from "user.slint";
    import { User2Struct } from "user2.slint";
    import { F-o-o as User3Fxx } from "user3.slint";
    import { User4Fxx } from "user4.slint";

    export component Main {
        property <F-o_o> main-prop: F_o_o.M1;
        property <User3Fxx> main-prop2: User3Fxx.M1;
        property <User2Struct> main-prop3;
        property <User3Fxx> main-prop4 <=> uc.user-component-prop;

        uc := UserComponent { }
    }
                    "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
    export enum F_o-o /* <- TEST_ME_1 */ { M1, M2, }
                    "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user.slint")).unwrap(),
                    r#"
    import { F-o_o as B_a-r } from "source.slint";

    export component UserComponent {
        in-out property <B_a-r> user-component-prop: B-a-r.M1;
    }

    export { B-a-r }
                    "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user2.slint")).unwrap(),
                    r#"
    import { F_o_o as XxxYyyZzz } from "source.slint";

    export struct User2Struct {
        member: XxxYyyZzz,
    }
                    "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user3.slint")).unwrap(),
                    r#"
    import { F-o-o } from "source.slint";

    export { F_o_o }
                    "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user4.slint")).unwrap(),
                    r#"
    import { F-o-o } from "source.slint";

    export { F_o_o as User4Fxx }
                    "#
                    .to_string(),
                ),
            ]),
            false,
        );

        let edited_text =
            rename_tester(&document_cache, &test::test_file_name("source.slint"), "_1");

        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains("import { XxxYyyZzz } from \"source.slint\""));
                assert!(ed.contents.contains("import { UserComponent } from \"user.slint\""));
                assert!(ed.contents.contains("import { User2Struct } from \"user2.slint\""));
                assert!(
                    ed.contents.contains("import { XxxYyyZzz as User3Fxx } from \"user3.slint\"")
                );
                assert!(ed.contents.contains("import { User4Fxx } from \"user4.slint\""));
                assert!(ed.contents.contains("property <XxxYyyZzz> main-prop: XxxYyyZzz.M1"));
                assert!(ed.contents.contains("property <User3Fxx> main-prop2: User3Fxx.M1"));
                assert!(ed.contents.contains("property <User2Struct> main-prop3;"));
                assert!(
                    ed.contents
                        .contains("property <User3Fxx> main-prop4 <=> uc.user-component-prop;")
                );
                assert!(ed.contents.contains("uc := UserComponent"));
            } else if ed_path == test::test_file_name("source.slint") {
                assert!(ed.contents.contains("export enum XxxYyyZzz /* <- TEST_ME_1 "));
            } else if ed_path == test::test_file_name("user.slint") {
                assert!(ed.contents.contains("import { XxxYyyZzz as B_a-r }"));
                assert!(ed.contents.contains("property <B_a-r> user-component-prop"));
                assert!(ed.contents.contains("> user-component-prop: B-a-r.M1;"));
                assert!(ed.contents.contains("export { B-a-r }"));
            } else if ed_path == test::test_file_name("user2.slint") {
                assert!(ed.contents.contains("import { XxxYyyZzz } from \"source.slint\""));
                assert!(ed.contents.contains("export struct User2Struct {"));
                assert!(ed.contents.contains("member: XxxYyyZzz,"));
            } else if ed_path == test::test_file_name("user3.slint") {
                assert!(ed.contents.contains("import { XxxYyyZzz }"));
                assert!(ed.contents.contains("export { XxxYyyZzz }"));
            } else if ed_path == test::test_file_name("user4.slint") {
                assert!(ed.contents.contains("import { XxxYyyZzz }"));
                assert!(ed.contents.contains("export { XxxYyyZzz as User4Fxx }"));
            } else {
                unreachable!();
            }
        }
    }

    #[track_caller]
    fn find_declaration_node_by_comment(
        document_cache: &common::DocumentCache,
        document_path: &Path,
        suffix: &str,
    ) -> DeclarationNode {
        let name = find_node_by_comment(document_cache, document_path, suffix);
        find_declaration_node(document_cache, &main_identifier(&name).unwrap()).unwrap()
    }

    #[test]
    fn test_rename_component_from_use() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
export { Foo /* <- TEST_ME_1 */ }

enum Xyz { Foo, Bar }

struct Abc { Foo: Xyz }

component Foo /* <- TEST_ME_TARGET */ inherits Rectangle {
    @children
}

component Baz {
    Foo /* <- TEST_ME_2 */ { }
}

struct Foo {
    bar: bool,
}

export component Bar inherits Foo /* <- TEST_ME_3 */ {
    Foo /* <- TEST_ME_4 */ { }
    Rectangle {
        Foo /* <- TEST_ME_5 */ { }
        Foo := Baz { }
    }

    if true: Rectangle {
        Foo /* <- TEST_ME_6 */ { }
    }

    if false: Rectangle {
        Foo /* <- TEST_ME_7 */ { }
    }

    function Foo(Foo: int) { Foo + 1; }
    function F() { self.Foo(42); }

    for i in [1, 2, 3]: Foo /* <- TEST_ME_8 */ { }
}
                "#
                .to_string(),
            )]),
            true, // Component `Foo` is replacing a component with the same name
        );

        let target =
            find_token_by_comment(&document_cache, &test::main_test_file_name(), "_TARGET");

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_1");
        id.query.token_info.is_same_symbol(&document_cache, target.clone());

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_2");
        id.query.token_info.is_same_symbol(&document_cache, target.clone());

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_3");
        id.query.token_info.is_same_symbol(&document_cache, target.clone());

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_4");
        id.query.token_info.is_same_symbol(&document_cache, target.clone());

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_5");
        id.query.token_info.is_same_symbol(&document_cache, target.clone());

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_6");
        id.query.token_info.is_same_symbol(&document_cache, target.clone());

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_7");
        id.query.token_info.is_same_symbol(&document_cache, target.clone());

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_8");
        id.query.token_info.is_same_symbol(&document_cache, target);
    }

    #[test]
    fn test_rename_struct_from_use() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
struct Foo /* <- TEST_ME_DECL */ {
    test: bool,
}

struct Bar {
    bar-test: Foo /* <- TEST_ME_1 */,
}

export component Bar {
    property <Foo /* <- TEST_ME_2 */> bar-prop: { test: false };
}
                    "#
                .to_string(),
            )]),
            false,
        );

        let declaration = find_declaration_node(
            &document_cache,
            &find_token_by_comment(&document_cache, &test::main_test_file_name(), "_DECL"),
        )
        .unwrap()
        .query
        .token_info
        .token
        .clone();

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_1");
        id.query.token_info.is_same_symbol(&document_cache, declaration.clone());

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_2");
        id.query.token_info.is_same_symbol(&document_cache, declaration);
    }

    #[test]
    fn test_rename_component_from_use_with_export() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
    import { F-o_o /* <- TEST_ME_IMPORT1 */ } from "source.slint";
    import { UserComponent } from "user.slint";
    import { User2Component } from "user2.slint";
    import { F-o-o /* <- TEST_ME_IMPORT2 */ as User3Fxx /* <- TEST_ME_IN1 */ } from "user3.slint";
    import { User4Fxx } from "user4.slint";

    export component Main {
        F_o_o /* <- TEST_ME_1 */ { }
        UserComponent { }
        User2Component { }
        User3Fxx /* <- TEST_ME_2 */ { }
        User4Fxx /* <- TEST_ME_3 */ { }
    }
                    "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
    export component F_o-o /* <- TEST_ME_DEF1 */ { @children }
                    "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user.slint")).unwrap(),
                    r#"
    import { F-o-o /* <- TEST_ME_IMPORT3 */ as Bar } from "source.slint";

    export component UserComponent {
        Bar /* <- TEST_ME_4 */ { }
    }

    export { Bar }
                    "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user2.slint")).unwrap(),
                    r#"
    import { F_o_o /* <- TEST_ME_IMPORT4 */ as XxxYyyZzz } from "source.slint";

    export component User2Component {
        XxxYyyZzz /* <- TEST_ME_5 */ { }
    }
                    "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user3.slint")).unwrap(),
                    r#"
    import { F-o_o /* <- TEST_ME_IMPORT5 */ } from "source.slint";

    export { F_o-o /* <- TEST_ME_EXPORT1 */ }
                    "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user4.slint")).unwrap(),
                    r#"
    import { F-o_o /* <- TEST_ME_IMPORT6 */ } from "source.slint";

    export { F_o-o /* <- TEST_ME_EXPORT2 */ as User4Fxx /* <- TEST_ME_EXT1 */}
                    "#
                    .to_string(),
                ),
            ]),
            false,
        );

        let declaration =
            find_token_by_comment(&document_cache, &test::test_file_name("source.slint"), "_DEF1");

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_1");
        id.query.token_info.is_same_symbol(&document_cache, declaration.clone());

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_2");
        let internal_name =
            find_token_by_comment(&document_cache, &test::main_test_file_name(), "_IN1");
        id.query.token_info.is_same_symbol(&document_cache, internal_name);

        let export_name =
            find_token_by_comment(&document_cache, &test::test_file_name("user4.slint"), "_EXT1");

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_3");
        id.query.token_info.is_same_symbol(&document_cache, export_name);
    }

    #[test]
    fn test_rename_struct_from_use_with_export() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { Foo /* <- TEST_ME_IMPORT1 */ } from "source.slint";
import { UserComponent } from "user.slint";
import { User2Struct } from "user2.slint";
import { Foo /* <- TEST_ME_IMPORT2 */ as User3Fxx /* <- TEST_ME_IN1 */} from "user3.slint";
import { User4Fxx } from "user4.slint";

export component Main {
    property <Foo /* <- TEST_ME_1 */> main-prop;
    property <User3Fxx /* <- TEST_ME_2 */> main-prop2;
    property <User2Struct> main-prop3;
    property <User4Fxx /* <- TEST_ME_3 */> main-prop4 <=> uc.user-component-prop;

    property <bool> test: main-prop3.member.test_me;

    uc := UserComponent { }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
export struct Foo /* <- TEST_ME_DEF1 */ { test-me: bool, }
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user.slint")).unwrap(),
                    r#"
import { Foo /* <- TEST_ME_IMPORT1 */ as Bar } from "source.slint";


export component UserComponent {
    in-out property <Bar> user-component-prop;
}

export { Bar }
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user2.slint")).unwrap(),
                    r#"
import { Foo /* <- TEST_ME_IMPORT2 */ as XxxYyyZzz } from "source.slint";

export struct User2Struct {
    member: XxxYyyZzz,
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user3.slint")).unwrap(),
                    r#"
import { Foo /* <- TEST_ME_IMPORT3 */} from "source.slint";

export { Foo /* <- TEST_ME_EXPORT1 */}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user4.slint")).unwrap(),
                    r#"
import { Foo /* <- TEST_ME_IMPORT4 */ } from "source.slint";

export { Foo /* <- TEST_ME_EXPORT2 */ as User4Fxx /* <- TEST_ME_EN1 */}
                "#
                    .to_string(),
                ),
            ]),
            false,
        );

        let declaration =
            find_token_by_comment(&document_cache, &test::test_file_name("source.slint"), "_DEF1");

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_1");
        id.query.token_info.is_same_symbol(&document_cache, declaration);

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_2");
        let internal_name =
            find_token_by_comment(&document_cache, &test::main_test_file_name(), "_IN1");
        id.query.token_info.is_same_symbol(&document_cache, internal_name);

        let export_name =
            find_token_by_comment(&document_cache, &test::test_file_name("user4.slint"), "_EN1");

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_3");
        id.query.token_info.is_same_symbol(&document_cache, export_name);
    }

    #[test]
    fn test_rename_enum_from_use_with_export() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { Foo } from "source.slint";
import { UserComponent } from "user.slint";
import { User2Struct } from "user2.slint";
import { Foo as User3Fxx /* <- TEST_ME_IN1 */} from "user3.slint";
import { User4Fxx } from "user4.slint";

export component Main {
    property <Foo /* <- TEST_ME_1 */> main-prop;
    property <User3Fxx /* <- TEST_ME_2 */> main-prop2;
    property <User2Struct> main-prop3;
    property <User4Fxx /* <- TEST_ME_3 */> main-prop4 <=> uc.user-component-prop;

    property <bool> test: main-prop3.member == Foo/* <- TEST_ME_4 */.test;

    uc := UserComponent { }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
export enum Foo /* <- TEST_ME_DEF1 */ { test, }
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user.slint")).unwrap(),
                    r#"
import { Foo /* <- TEST_ME_IMPORT1 */ as Bar } from "source.slint";


export component UserComponent {
    in-out property <Bar> user-component-prop;
}

export { Bar }
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user2.slint")).unwrap(),
                    r#"
import { Foo /* <- TEST_ME_IMPORT2 */ as XxxYyyZzz } from "source.slint";

export struct User2Struct {
    member: XxxYyyZzz,
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user3.slint")).unwrap(),
                    r#"
import { Foo /* <- TEST_ME_IMPORT3 */} from "source.slint";

export { Foo /* <- TEST_ME_EXPORT1 */}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("user4.slint")).unwrap(),
                    r#"
import { Foo /* <- TEST_ME_IMPORT4 */ } from "source.slint";

export { Foo /* <- TEST_ME_EXPORT2 */ as User4Fxx /* <- TEST_ME_EN1 */}
                "#
                    .to_string(),
                ),
            ]),
            false,
        );

        let declaration =
            find_token_by_comment(&document_cache, &test::test_file_name("source.slint"), "_DEF1");

        let internal_name =
            find_token_by_comment(&document_cache, &test::main_test_file_name(), "_IN1");

        let export_name =
            find_token_by_comment(&document_cache, &test::test_file_name("user4.slint"), "_EN1");

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_1");
        id.query.token_info.is_same_symbol(&document_cache, declaration);

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_2");
        id.query.token_info.is_same_symbol(&document_cache, internal_name);

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_3");
        id.query.token_info.is_same_symbol(&document_cache, export_name.clone());

        let id =
            find_declaration_node_by_comment(&document_cache, &test::main_test_file_name(), "_4");
        id.query.token_info.is_same_symbol(&document_cache, export_name);
    }

    #[test]
    fn test_rename_import_from_internal_name() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { Foo as Bar /* <- TEST_ME_1 */ } from "source.slint";

export component Main {
    Bar { }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
export component Foo { }
                "#
                    .to_string(),
                ),
            ]),
            false,
        );

        let edited_text =
            rename_tester_with_new_name(&document_cache, &test::main_test_file_name(), "_1", "Baz");

        assert_eq!(edited_text.len(), 1);
        assert!(
            edited_text[0]
                .contents
                .contains("import { Foo as Baz /* <- TEST_ME_1 */ } from \"source.slint\";")
        );
        assert!(edited_text[0].contents.contains("component Main {"));
        assert!(edited_text[0].contents.contains(" Baz { "));

        let edited_text =
            rename_tester_with_new_name(&document_cache, &test::main_test_file_name(), "_1", "Foo");

        assert_eq!(edited_text.len(), 1);
        assert!(
            edited_text[0]
                .contents
                .contains("import { Foo /* <- TEST_ME_1 */ } from \"source.slint\";")
        );
        assert!(edited_text[0].contents.contains("component Main {"));
        assert!(edited_text[0].contents.contains(" Foo { "));
    }

    #[test]
    fn test_rename_import_from_external_name() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { Foo /* <- TEST_ME_1 */ as Bar } from "source.slint";

export component Main {
    Bar { }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
export component Foo { }
                "#
                    .to_string(),
                ),
            ]),
            false,
        );

        let edited_text =
            rename_tester_with_new_name(&document_cache, &test::main_test_file_name(), "_1", "Baz");

        assert_eq!(edited_text.len(), 2);

        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(
                    ed.contents.contains(
                        "import { Baz /* <- TEST_ME_1 */ as Bar } from \"source.slint\";"
                    )
                );
                assert!(ed.contents.contains("component Main {"));
                assert!(ed.contents.contains(" Bar { "));
            } else if ed_path == test::test_file_name("source.slint") {
                assert!(ed.contents.contains("export component Baz { }"));
            } else {
                panic!("Unexpected file!");
            }
        }

        let edited_text =
            rename_tester_with_new_name(&document_cache, &test::main_test_file_name(), "_1", "Bar");

        assert_eq!(edited_text.len(), 2);
        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains("import { Bar } from \"source.slint\";"));
                assert!(ed.contents.contains("component Main {"));
                assert!(ed.contents.contains(" Bar { "));
            } else if ed_path == test::test_file_name("source.slint") {
                assert!(ed.contents.contains("export component Bar { }"));
            } else {
                panic!("Unexpected file!");
            }
        }
    }

    #[test]
    fn test_rename_import_from_external_name_with_export_renaming() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { Foo /* <- TEST_ME_1 */ as Bar } from "source.slint";

export component Main {
    Bar { }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
component XxxYyyZzz { }

export { XxxYyyZzz as Foo }
                "#
                    .to_string(),
                ),
            ]),
            false,
        );

        let edited_text = rename_tester_with_new_name(
            &document_cache,
            &test::main_test_file_name(),
            "_1",
            "XFooX",
        );

        assert_eq!(edited_text.len(), 2);

        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(
                    ed.contents.contains(
                        "import { XFooX /* <- TEST_ME_1 */ as Bar } from \"source.slint\";"
                    )
                );
                assert!(ed.contents.contains("component Main {"));
                assert!(ed.contents.contains(" Bar { "));
            } else if ed_path == test::test_file_name("source.slint") {
                assert!(ed.contents.contains("component XxxYyyZzz { }"));
                assert!(ed.contents.contains("export { XxxYyyZzz as XFooX }"));
            } else {
                panic!("Unexpected file!");
            }
        }

        let edited_text =
            rename_tester_with_new_name(&document_cache, &test::main_test_file_name(), "_1", "Bar");

        assert_eq!(edited_text.len(), 2);

        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains("import { Bar } from \"source.slint\";"));
                assert!(ed.contents.contains("component Main {"));
                assert!(ed.contents.contains(" Bar { "));
            } else if ed_path == test::test_file_name("source.slint") {
                assert!(ed.contents.contains("component XxxYyyZzz { }"));
                assert!(ed.contents.contains("export { XxxYyyZzz as Bar }"));
            } else {
                panic!("Unexpected file!");
            }
        }

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");

        assert_eq!(edited_text.len(), 2);
        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains(
                    "import { XxxYyyZzz /* <- TEST_ME_1 */ as Bar } from \"source.slint\";"
                ));
                assert!(ed.contents.contains("component Main {"));
                assert!(ed.contents.contains(" Bar { "));
            } else if ed_path == test::test_file_name("source.slint") {
                assert!(ed.contents.contains("component XxxYyyZzz { }"));
                assert!(ed.contents.contains("export { XxxYyyZzz }"));
            } else {
                panic!("Unexpected file!");
            }
        }
    }

    #[test]
    fn test_rename_property_from_definition() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
component re_name-me {
    property <bool> re_name-me /* <- TEST_ME_1 */: true;

    function re_name-me_(re-name_me: int) { /* 1 */ self.re-name_me = re-name_me >= 42; }
}

export component Bar {
    property <bool> re_name-me /* <- TEST_ME_2 */: true;

    function re_name-me_(re-name_me: int) { /* 2 */ self.re-name_me = re-name_me >= 42; }

    re_name-me { }
}
                "#
                .to_string(),
            )]),
            true, // Component `Foo` is replacing a component with the same name
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");
        assert_eq!(edited_text.len(), 1);

        assert_eq!(edited_text.len(), 1);
        assert!(edited_text[0].contents.contains("component re_name-me {"));
        assert!(edited_text[0].contents.contains("property <bool> XxxYyyZzz /* <- TEST_ME_1 "));
        assert!(edited_text[0].contents.contains(" <- TEST_ME_1 */: true;"));
        assert!(
            edited_text[0].contents.contains("function re_name-me_(re-name_me: int) { /* 1 */")
        );
        assert!(edited_text[0].contents.contains("/* 1 */ self.XxxYyyZzz = re-name_me >= 42;"));

        assert!(edited_text[0].contents.contains("export component Bar {"));
        assert!(edited_text[0].contents.contains("property <bool> re_name-me /* <- TEST_ME_2 "));
        assert!(edited_text[0].contents.contains(" <- TEST_ME_2 */: true;"));
        assert!(
            edited_text[0].contents.contains("function re_name-me_(re-name_me: int) { /* 2 */")
        );
        assert!(edited_text[0].contents.contains("/* 2 */ self.re-name_me = re-name_me >= 42;"));
        assert!(edited_text[0].contents.contains("re_name-me { }"));

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_2");
        assert_eq!(edited_text.len(), 1);

        assert_eq!(edited_text.len(), 1);
        assert!(edited_text[0].contents.contains("component re_name-me {"));
        assert!(edited_text[0].contents.contains("property <bool> re_name-me /* <- TEST_ME_1 "));
        assert!(edited_text[0].contents.contains(" <- TEST_ME_1 */: true;"));
        assert!(
            edited_text[0].contents.contains("function re_name-me_(re-name_me: int) { /* 1 */")
        );
        assert!(edited_text[0].contents.contains("/* 1 */ self.re-name_me = re-name_me >= 42;"));

        assert!(edited_text[0].contents.contains("export component Bar {"));
        assert!(edited_text[0].contents.contains("property <bool> XxxYyyZzz /* <- TEST_ME_2 "));
        assert!(edited_text[0].contents.contains(" <- TEST_ME_2 */: true;"));
        assert!(
            edited_text[0].contents.contains("function re_name-me_(re-name_me: int) { /* 2 */")
        );
        assert!(edited_text[0].contents.contains("/* 2 */ self.XxxYyyZzz = re-name_me >= 42;"));
        assert!(edited_text[0].contents.contains("re_name-me { }"));
    }

    #[test]
    fn test_rename_property_from_use() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
component re_name-me {
    property <bool> re_name-me /* 1 */: true;

    function re_name-me_(re-name_me: int) { /* 1 */ self.re-name_me /* <- TEST_ME_1 */ = re-name_me >= 42; }
}

export component Bar {
    property <bool> re_name-me /* 2 */: true;

    function re_name-me_(re-name_me: int) { /* 2 */ self.re-name_me /* <- TEST_ME_2 */ = re-name_me >= 42; }

    re_name-me { }
}
                "#
                .to_string(),
            )]),
            true, // Component `Foo` is replacing a component with the same name
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");
        assert_eq!(edited_text.len(), 1);

        assert_eq!(edited_text.len(), 1);
        assert!(edited_text[0].contents.contains("component re_name-me {"));
        assert!(edited_text[0].contents.contains("property <bool> XxxYyyZzz /* 1 */"));
        assert!(edited_text[0].contents.contains(" 1 */: true;"));
        assert!(
            edited_text[0].contents.contains("function re_name-me_(re-name_me: int) { /* 1 */")
        );
        assert!(
            edited_text[0]
                .contents
                .contains("/* 1 */ self.XxxYyyZzz /* <- TEST_ME_1 */ = re-name_me >= 42;")
        );

        assert!(edited_text[0].contents.contains("export component Bar {"));
        assert!(edited_text[0].contents.contains("property <bool> re_name-me /* 2 "));
        assert!(edited_text[0].contents.contains(" 2 */: true;"));
        assert!(
            edited_text[0].contents.contains("function re_name-me_(re-name_me: int) { /* 2 */")
        );
        assert!(
            edited_text[0]
                .contents
                .contains("/* 2 */ self.re-name_me /* <- TEST_ME_2 */ = re-name_me >= 42;")
        );
        assert!(edited_text[0].contents.contains("re_name-me { }"));

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_2");
        assert_eq!(edited_text.len(), 1);

        assert_eq!(edited_text.len(), 1);
        assert!(edited_text[0].contents.contains("component re_name-me {"));
        assert!(edited_text[0].contents.contains("property <bool> re_name-me /* 1 "));
        assert!(edited_text[0].contents.contains(" 1 */: true;"));
        assert!(
            edited_text[0].contents.contains("function re_name-me_(re-name_me: int) { /* 1 */")
        );
        assert!(
            edited_text[0]
                .contents
                .contains("/* 1 */ self.re-name_me /* <- TEST_ME_1 */ = re-name_me >= 42;")
        );

        assert!(edited_text[0].contents.contains("export component Bar {"));
        assert!(edited_text[0].contents.contains("property <bool> XxxYyyZzz /* 2 "));
        assert!(edited_text[0].contents.contains(" 2 */: true;"));
        assert!(
            edited_text[0].contents.contains("function re_name-me_(re-name_me: int) { /* 2 */")
        );
        assert!(
            edited_text[0]
                .contents
                .contains("/* 2 */ self.XxxYyyZzz /* <- TEST_ME_2 */ = re-name_me >= 42;")
        );
        assert!(edited_text[0].contents.contains("re_name-me { }"));
    }

    #[test]
    fn test_rename_property_from_definition_with_export() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { re_name-me } from "source.slint";

export component Bar {
    property <bool> re_name-me /* 1 */ : true;

    function re_name-me_(re_name-me: int) { /* 2 */ self.re-name_me = re-name_me >= 42; }

    re_name-me {
        re_name-me/* 3 */: false;
    }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
export component re_name-me {
    in-out property <bool> re_name-me /* <- TEST_ME_1 */: true;

    function re_name-me_(re_name-me: int) { /* 4 */ self.re-name_me = re-name_me >= 42; }
}
                "#
                    .to_string(),
                ),
            ]),
            true, // Component `Foo` is replacing a component with the same name
        );

        let edited_text =
            rename_tester(&document_cache, &test::test_file_name("source.slint"), "_1");

        assert_eq!(edited_text.len(), 2);
        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains("import { re_name-me } from \"source.slint\";"));
                assert!(ed.contents.contains("export component Bar {"));
                assert!(ed.contents.contains("property <bool> re_name-me /* 1 */"));
                assert!(ed.contents.contains("function re_name-me_(re_name-me: int) { /* 2 */"));
                assert!(ed.contents.contains("/* 2 */ self.re-name_me = re-name_me >= 42;"));
                assert!(ed.contents.contains("re_name-me {"));
                assert!(ed.contents.contains("XxxYyyZzz/* 3 */:"));
            } else if ed_path == test::test_file_name("source.slint") {
                assert!(ed.contents.contains("export component re_name-me {"));
                assert!(ed.contents.contains("in-out property <bool> XxxYyyZzz /* <- TEST_ME_1"));
                assert!(ed.contents.contains("function re_name-me_(re_name-me: int) { /* 4 */"));
                assert!(ed.contents.contains("/* 4 */ self.XxxYyyZzz = re-name_me >= 42;"));
            } else {
                panic!("Unexpected file!");
            }
        }
    }

    #[test]
    fn test_rename_property_from_use_with_export() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { re_name-me } from "source.slint";

export component Bar {
    property <bool> re_name-me /* 1 */ : true;

    function re_name-me_(re_name-me: int) { /* 2 */ self.re-name_me = re-name_me >= 42; }

    re_name-me {
        re_name-me/* <- TEST_ME_1 */: false;
    }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
export component re_name-me {
    in-out property <bool> re_name-me /* 3 */: true;

    function re_name-me_(re_name-me: int) { /* 4 */ self.re-name_me = re_name-me >= 42; }
}
                "#
                    .to_string(),
                ),
            ]),
            true, // Component `Foo` is replacing a component with the same name
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");

        assert_eq!(edited_text.len(), 2);
        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains("import { re_name-me } from \"source.slint\";"));
                assert!(ed.contents.contains("export component Bar {"));
                assert!(ed.contents.contains("property <bool> re_name-me /* 1 */"));
                assert!(ed.contents.contains("function re_name-me_(re_name-me: int) "));
                assert!(ed.contents.contains("{ /* 2 */ self.re-name_me = re-name_me >= 42;"));
                assert!(ed.contents.contains("re_name-me {"));
                assert!(ed.contents.contains("XxxYyyZzz/* <- TEST_ME_1 */:"));
            } else if ed_path == test::test_file_name("source.slint") {
                assert!(ed.contents.contains("export component re_name-me {"));
                assert!(ed.contents.contains("in-out property <bool> XxxYyyZzz /* 3 */"));
                assert!(ed.contents.contains("function re_name-me_(re_name-me: int) { /* 4 */"));
                assert!(ed.contents.contains("/* 4 */ self.XxxYyyZzz = re_name-me >= 42;"));
            } else {
                panic!("Unexpected file!");
            }
        }
    }

    #[test]
    fn test_rename_property_in_subelement() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
component Baz {
    in-out property <int> re_name-me /* <- TEST_ME_1 */: 42;
}

export component Bar {
    baz := Baz {
        re_name-me /* <- TEST_ME_2 */: 23;
    }
    init => {
        baz.re_name-me = 7;
    }
}
                "#
                .to_string(),
            )]),
            true, // Component `Foo` is replacing a component with the same name
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");

        assert_eq!(edited_text.len(), 1);

        assert!(edited_text[0].contents.contains("component Baz {"));
        assert!(edited_text[0].contents.contains("<int> XxxYyyZzz /* <- TEST_ME_1 */: 42;"));
        assert!(edited_text[0].contents.contains("    XxxYyyZzz /* <- TEST_ME_2 */: 23;"));

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_2");

        assert_eq!(edited_text.len(), 1);

        assert!(edited_text[0].contents.contains("component Baz {"));
        assert!(edited_text[0].contents.contains("<int> XxxYyyZzz /* <- TEST_ME_1 */: 42;"));
        assert!(edited_text[0].contents.contains("    XxxYyyZzz /* <- TEST_ME_2 */: 23;"));
    }

    #[test]
    fn test_rename_property_defined_in_subelement() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
export component Bar {
    id := Rectangle {
        in-out property <int> re_name-me /* <- TEST_ME_1 */: 42;
    }

    in-out property <int> usage: id.re_name-me /* <- TEST_ME_2 */;
}
                "#
                .to_string(),
            )]),
            true, // Component `Foo` is replacing a component with the same name
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");

        assert_eq!(edited_text.len(), 1);

        assert!(edited_text[0].contents.contains("export component Bar {"));
        assert!(edited_text[0].contents.contains("<int> XxxYyyZzz /* <- TEST_ME_1 */: 42;"));
        assert!(edited_text[0].contents.contains("usage: id.XxxYyyZzz /* <- TEST_ME_2 */;"));

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_2");

        assert_eq!(edited_text.len(), 1);

        assert!(edited_text[0].contents.contains("export component Bar {"));
        assert!(edited_text[0].contents.contains("<int> XxxYyyZzz /* <- TEST_ME_1 */: 42;"));
        assert!(edited_text[0].contents.contains("usage: id.XxxYyyZzz /* <- TEST_ME_2 */;"));
    }

    #[test]
    fn test_rename_callback() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
struct re_name-me {}
component re_name-me {
    callback re_name-me(re_name-me : re_name-me) -> re_name-me;

    re_name_me => {
        re-name-me({});
    }
}

export component Bar {
    property <bool> re_name-me: true;

    re_name-me := re_name-me {
        re_name_me /* <- TEST_ME_1 */ => {
            let re-name-me = 0;
            root.re_name-me = false;
            self.re-name-me({});
        }
    }
}
"#
                .to_string(),
            )]),
            false,
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");
        assert_eq!(edited_text.len(), 1);

        assert_eq!(edited_text.len(), 1);
        assert_eq!(
            edited_text[0].contents,
            r#"
struct re_name-me {}
component re_name-me {
    callback XxxYyyZzz(re_name-me : re_name-me) -> re_name-me;

    XxxYyyZzz => {
        XxxYyyZzz({});
    }
}

export component Bar {
    property <bool> re_name-me: true;

    re_name-me := re_name-me {
        XxxYyyZzz /* <- TEST_ME_1 */ => {
            let re-name-me = 0;
            root.re_name-me = false;
            self.XxxYyyZzz({});
        }
    }
}
"#
        );
    }

    #[test]
    fn test_rename_function() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
global X {
    public function re_name_me /* <- TEST_ME_1 */ (re_name-me: int) {
        debug(re_name-me);
    }
}
global re-name-me {}
export component Bar {
    public function re-name-me(re_name-me: int) {
        X.re_name-me(re_name-me);
    }
}
"#
                .to_string(),
            )]),
            false,
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");
        assert_eq!(edited_text.len(), 1);

        assert_eq!(edited_text.len(), 1);
        assert_eq!(
            edited_text[0].contents,
            r#"
global X {
    public function XxxYyyZzz /* <- TEST_ME_1 */ (re_name-me: int) {
        debug(re_name-me);
    }
}
global re-name-me {}
export component Bar {
    public function re-name-me(re_name-me: int) {
        X.XxxYyyZzz(re_name-me);
    }
}
"#
        );
    }

    #[test]
    fn test_rename_globals_from_definition() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
export { Foo }

global Foo /* <- TEST_ME_1 */ {
    in property <bool> test-property: true;
}

export component Bar {
    function baz(bar: int) -> bool { return Foo.test_property && bar >= 42; }
}
                "#
                .to_string(),
            )]),
            true, // Component `Foo` is replacing a component with the same name
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");

        assert_eq!(edited_text.len(), 1);
        assert!(edited_text[0].contents.contains("export { XxxYyyZzz }"));
        assert!(edited_text[0].contents.contains("global XxxYyyZzz /* <- TEST_ME_1 "));
        assert!(edited_text[0].contents.contains("in property <bool> test-property: true;"));
        assert!(edited_text[0].contents.contains("function baz(bar: int)"));
        assert!(
            edited_text[0]
                .contents
                .contains("int) -> bool { return XxxYyyZzz.test_property && bar >= 42")
        );
    }

    #[test]
    fn test_rename_globals_from_use() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([(
                Url::from_file_path(test::main_test_file_name()).unwrap(),
                r#"
export { Foo }

global Foo {
    in property <bool> test-property: true;
}

export component Bar {
    function baz(bar: int) -> bool { return Foo /* <- TEST_ME_1 */.test_property && bar >= 42; }
}
                "#
                .to_string(),
            )]),
            true, // Component `Foo` is replacing a component with the same name
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");

        assert_eq!(edited_text.len(), 1);
        assert!(edited_text[0].contents.contains("export { XxxYyyZzz }"));
        assert!(edited_text[0].contents.contains("global XxxYyyZzz {"));
        assert!(edited_text[0].contents.contains("in property <bool> test-property: true;"));
        assert!(edited_text[0].contents.contains("function baz(bar: int)"));
        assert!(edited_text[0].contents.contains(
            "int) -> bool { return XxxYyyZzz /* <- TEST_ME_1 */.test_property && bar >= 42"
        ));
    }

    #[test]
    fn test_rename_globals_from_use_with_export() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { Foo } from "source.slint";

export component Bar {
    function baz(bar: int) -> bool { return Foo /* <- TEST_ME_1 */.test_property && bar >= 42; }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
export { Foo }

global Foo {
    in property <bool> test-property: true;
}
                "#
                    .to_string(),
                ),
            ]),
            true, // Component `Foo` is replacing a component with the same name
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");

        assert_eq!(edited_text.len(), 2);
        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains("import { XxxYyyZzz } from \"source.slint\";"));
                assert!(ed.contents.contains("function baz(bar: int)"));
                assert!(ed.contents.contains(
                    "int) -> bool { return XxxYyyZzz /* <- TEST_ME_1 */.test_property && bar >= 42"
                ));
            } else if ed_path == test::test_file_name("source.slint") {
                assert!(ed.contents.contains("export { XxxYyyZzz }"));
                assert!(ed.contents.contains("global XxxYyyZzz {"));
                assert!(ed.contents.contains("in property <bool> test-property: true;"));
            } else {
                panic!("Unexpected file!");
            }
        }
    }

    #[test]
    fn test_rename_globals_from_use_with_export_module() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { Foo } from "reexport.slint";

export component Bar {
    function baz(bar: int) -> bool { return Foo /* <- TEST_ME_1 */.test_property && bar >= 42; }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("reexport.slint")).unwrap(),
                    r#"
export { Foo } from "source.slint";
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
export { Foo }

global Foo {
    in property <bool> test-property: true;
}
                "#
                    .to_string(),
                ),
            ]),
            true, // Component `Foo` is replacing a component with the same name
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");

        assert_eq!(edited_text.len(), 3);
        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains("import { XxxYyyZzz } from \"reexport.slint\";"));
                assert!(ed.contents.contains("function baz(bar: int)"));
                assert!(ed.contents.contains(
                    "int) -> bool { return XxxYyyZzz /* <- TEST_ME_1 */.test_property && bar >= 42"
                ));
            } else if ed_path == test::test_file_name("source.slint") {
                assert!(ed.contents.contains("export { XxxYyyZzz }"));
                assert!(ed.contents.contains("global XxxYyyZzz {"));
                assert!(ed.contents.contains("in property <bool> test-property: true;"));
            } else if ed_path == test::test_file_name("reexport.slint") {
                assert!(ed.contents.contains("export { XxxYyyZzz } from \"source.slint\""));
            } else {
                panic!("Unexpected file!");
            }
        }
    }

    #[test]
    fn test_rename_globals_from_use_with_export_module_renamed() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { Foobar } from "reexport.slint";

export component Bar {
    function baz(bar: int) -> bool { return Foobar /* <- TEST_ME_1 */.test_property && bar >= 42; }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("reexport.slint")).unwrap(),
                    r#"
export { Foo /* <- TEST_ME_2 */ as Foobar } from "source.slint";
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
export { Foo }

global Foo {
    in property <bool> test-property: true;
}
                "#
                    .to_string(),
                ),
            ]),
            true, // Component `Foo` is replacing a component with the same name
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");

        assert_eq!(edited_text.len(), 2);
        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains("import { XxxYyyZzz } from \"reexport.slint\";"));
                assert!(ed.contents.contains("function baz(bar: int)"));
                assert!(ed.contents.contains(
                    "int) -> bool { return XxxYyyZzz /* <- TEST_ME_1 */.test_property && bar >= 42"
                ));
            } else if ed_path == test::test_file_name("reexport.slint") {
                assert!(ed.contents.contains(
                    "export { Foo /* <- TEST_ME_2 */ as XxxYyyZzz } from \"source.slint\""
                ));
            } else {
                panic!("Unexpected file!");
            }
        }

        let edited_text = rename_tester_with_new_name(
            &document_cache,
            &test::test_file_name("reexport.slint"),
            "_2",
            "Foobar",
        );

        assert_eq!(edited_text.len(), 2);
        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::test_file_name("source.slint") {
                assert!(ed.contents.contains("export { Foobar }"));
                assert!(ed.contents.contains("global Foobar {"));
                assert!(ed.contents.contains("in property <bool> test-property: true;"));
            } else if ed_path == test::test_file_name("reexport.slint") {
                assert!(ed.contents.contains("export { Foobar } from \"source.slint\""));
            } else {
                panic!("Unexpected file!");
            }
        }
    }

    #[test]
    fn test_rename_globals_from_use_with_export_module_star() {
        let document_cache = test::compile_test_with_sources(
            "fluent",
            HashMap::from([
                (
                    Url::from_file_path(test::main_test_file_name()).unwrap(),
                    r#"
import { Foo } from "reexport.slint";

export component Bar {
    function baz(bar: int) -> bool { return Foo /* <- TEST_ME_1 */.test_property && bar >= 42; }
}
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("reexport.slint")).unwrap(),
                    r#"
export * from "source.slint";
                "#
                    .to_string(),
                ),
                (
                    Url::from_file_path(test::test_file_name("source.slint")).unwrap(),
                    r#"
export { Foo }

global Foo {
    in property <bool> test-property: true;
}
                "#
                    .to_string(),
                ),
            ]),
            true, // Component `Foo` is replacing a component with the same name
        );

        let edited_text = rename_tester(&document_cache, &test::main_test_file_name(), "_1");

        assert_eq!(edited_text.len(), 2);
        for ed in &edited_text {
            let ed_path = ed.url.to_file_path().unwrap();
            if ed_path == test::main_test_file_name() {
                assert!(ed.contents.contains("import { XxxYyyZzz } from \"reexport.slint\";"));
                assert!(ed.contents.contains("function baz(bar: int)"));
                assert!(ed.contents.contains(
                    "int) -> bool { return XxxYyyZzz /* <- TEST_ME_1 */.test_property && bar >= 42"
                ));
            } else if ed_path == test::test_file_name("source.slint") {
                assert!(ed.contents.contains("export { XxxYyyZzz }"));
                assert!(ed.contents.contains("global XxxYyyZzz {"));
                assert!(ed.contents.contains("in property <bool> test-property: true;"));
            } else {
                panic!("Unexpected file!");
            }
        }
    }
}

/// Integration tests for the classification and search portions of the
/// cross-language rename pipeline:
/// `find_declaration_node` -> `rename()` -> `host_language_classification()`
/// -> `search_replace_host_language_accessors()`.
/// Tests for the interactive LSP requests live in `language.rs`.
#[cfg(test)]
mod host_language_rename_tests {
    use super::*;
    use crate::common;
    use crate::common::host_language_search::{ScanBounds, search_replace_host_language_accessors};
    use i_slint_compiler::diagnostics::{BuildDiagnostics, ByteFormat};
    use lsp_types::{Url, WorkspaceEdit, WorkspaceFolder};
    use std::collections::HashMap;
    use std::path::PathBuf;

    /// Throwaway tempdir; cleans itself up on drop.
    struct TempDir {
        path: PathBuf,
    }

    impl TempDir {
        fn path(&self) -> &std::path::Path {
            &self.path
        }
    }

    impl Drop for TempDir {
        fn drop(&mut self) {
            let _ = std::fs::remove_dir_all(&self.path);
        }
    }

    fn tempdir() -> TempDir {
        use std::sync::atomic::{AtomicUsize, Ordering};
        // Per-test serial -- bare timestamps collide when many tests start
        // in the same nanosecond under cargo's parallel runner.
        static COUNTER: AtomicUsize = AtomicUsize::new(0);
        let serial = COUNTER.fetch_add(1, Ordering::Relaxed);
        let mut path = std::env::temp_dir();
        path.push(format!(
            "slint-lsp-rename-roundtrip-{}-{}-{serial}",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .map(|d| d.as_nanos())
                .unwrap_or(0),
        ));
        std::fs::create_dir(&path).unwrap();
        TempDir { path }
    }

    /// Build a workspace under `tempdir`: writes the given `.slint` files and
    /// `.rs` files to disk, then loads the slint files into a `DocumentCache`.
    /// The first slint file in `slint_files` is treated as the "main" one
    /// and its URL is returned.
    ///
    /// The second arg to load_url is the source version; we use 1 so that
    /// the produced WorkspaceEdit has a non-None document version on the
    /// slint side -- which the host scanner deliberately leaves as None for
    /// `.rs`/`.cpp` files (those aren't in the cache).
    fn setup(
        tmp: &TempDir,
        slint_files: &[(&str, &str)],
        host_files: &[(&str, &str)],
    ) -> (common::DocumentCache, Url, Vec<WorkspaceFolder>) {
        // Write all host-language files to disk. The scanner is language-
        // agnostic at this layer (same accessor strings emitted for Rust and
        // C++), so `host_files` may contain `.rs`, `.cpp`, `.h`, etc.
        for (rel_path, contents) in host_files {
            let full = tmp.path().join(rel_path);
            if let Some(parent) = full.parent() {
                std::fs::create_dir_all(parent).unwrap();
            }
            std::fs::write(&full, contents).unwrap();
        }

        // Build the DocumentCache.
        let config = common::document_cache::CompilerConfiguration {
            style: Some("fluent".into()),
            ..Default::default()
        };
        let mut cache = common::DocumentCache::new(config);
        spin_on::spin_on(cache.preload_builtins());

        // Write each slint file and load it into the cache.
        let mut main_url: Option<Url> = None;
        for (i, (rel_path, contents)) in slint_files.iter().enumerate() {
            let full = tmp.path().join(rel_path);
            if let Some(parent) = full.parent() {
                std::fs::create_dir_all(parent).unwrap();
            }
            std::fs::write(&full, contents).unwrap();
            let url = Url::from_file_path(&full).unwrap();
            let mut diag = BuildDiagnostics::default();
            spin_on::spin_on(cache.load_url(&url, Some(1), contents.to_string(), &mut diag))
                .unwrap();
            assert!(
                !diag.has_errors(),
                "slint diagnostics in {rel_path}: {:?}",
                diag.iter().map(|d| d.message().to_string()).collect::<Vec<_>>(),
            );
            if i == 0 {
                main_url = Some(url);
            }
        }

        let folders = vec![WorkspaceFolder {
            uri: Url::from_file_path(tmp.path()).unwrap(),
            name: "ws".into(),
        }];
        (cache, main_url.unwrap(), folders)
    }

    /// Locate the token immediately before a `/* <- TEST_ME<suffix> */` marker
    /// in the document at `url`.
    #[track_caller]
    fn find_token_in_url(
        document_cache: &common::DocumentCache,
        url: &Url,
        suffix: &str,
    ) -> SyntaxToken {
        let document = document_cache.get_document(url).unwrap();
        let document = document.node.as_ref().unwrap();
        let offset =
            document.text().to_string().find(&format!("<- TEST_ME{suffix} ")).unwrap() as u32;
        let comment = document.token_at_offset(offset.into()).next().unwrap();
        assert_eq!(comment.kind(), SyntaxKind::Comment);
        let mut token = comment.prev_token();
        while let Some(t) = &token {
            if ![SyntaxKind::Comment, SyntaxKind::Eof, SyntaxKind::Whitespace].contains(&t.kind()) {
                break;
            }
            token = t.prev_token();
        }
        token.unwrap()
    }

    /// Compose Slint and host-language edits for concise assertions.
    /// Production sends these as two independent workspace edits.
    fn perform_rename(
        document_cache: &common::DocumentCache,
        folders: &[WorkspaceFolder],
        slint_url: &Url,
        token_suffix: &str,
        new_name: &str,
    ) -> WorkspaceEdit {
        let token = find_token_in_url(document_cache, slint_url, token_suffix);
        let decl =
            find_declaration_node(document_cache, &token).expect("declaration node must be found");
        let mut workspace_edit = decl.rename(document_cache, new_name).expect("rename");

        if let Some(info) = decl.host_language_classification(document_cache)
            && i_slint_compiler::parser::normalize_identifier(new_name) != info.old_name
        {
            let host_edits = search_replace_host_language_accessors(
                folders,
                info.kind,
                &info.old_name,
                new_name,
                ByteFormat::Utf16,
                ScanBounds::DEFAULT,
            )
            .expect("scan");
            if !host_edits.is_empty() {
                let host_we = common::create_workspace_edit_from_single_text_edits(host_edits);
                common::merge_workspace_edits(&mut workspace_edit, host_we);
            }
        }
        workspace_edit
    }

    /// Collect the (URL -> [new_text]) map from a WorkspaceEdit's
    /// `document_changes::Edits` form, for assertion ergonomics.
    fn edits_by_url(edit: &WorkspaceEdit) -> HashMap<Url, Vec<String>> {
        let mut out: HashMap<Url, Vec<String>> = HashMap::new();
        if let Some(lsp_types::DocumentChanges::Edits(v)) = edit.document_changes.as_ref() {
            for tde in v {
                let url = tde.text_document.uri.clone();
                for e in &tde.edits {
                    if let lsp_types::OneOf::Left(te) = e {
                        out.entry(url.clone()).or_default().push(te.new_text.clone());
                    }
                }
            }
        }
        out
    }

    fn rs_url(tmp: &TempDir, rel: &str) -> Url {
        Url::from_file_path(tmp.path().join(rel)).unwrap()
    }

    fn slint_url(tmp: &TempDir, rel: &str) -> Url {
        Url::from_file_path(tmp.path().join(rel)).unwrap()
    }

    /// Property rename: `count` -> `total` in .slint should rewrite the
    /// `.slint` declaration AND the `get_count`/`set_count` accessors in
    /// the .rs file.
    #[test]
    fn property_rename_rewrites_slint_and_rust() {
        let tmp = tempdir();
        let (cache, url, folders) = setup(
            &tmp,
            &[(
                "ui/app.slint",
                r#"
export component App inherits Window {
    in property <int> count /* <- TEST_ME */;
}
                "#,
            )],
            &[("src/main.rs", "fn main() { let v = obj.get_count(); obj.set_count(v + 1); }\n")],
        );

        let edit = perform_rename(&cache, &folders, &url, "", "total");
        let by_url = edits_by_url(&edit);
        let rs = rs_url(&tmp, "src/main.rs");
        let slint = slint_url(&tmp, "ui/app.slint");
        assert!(by_url.contains_key(&slint), "missing .slint edits");
        let rust_edits = by_url.get(&rs).expect("missing .rs edits");
        assert!(rust_edits.contains(&"get_total".to_string()), "got: {rust_edits:?}");
        assert!(rust_edits.contains(&"set_total".to_string()), "got: {rust_edits:?}");
    }

    /// Callback rename: `clicked` -> `pressed` should rewrite `invoke_clicked`
    /// and `on_clicked` in the .rs file.
    #[test]
    fn callback_rename_rewrites_invoke_and_on() {
        let tmp = tempdir();
        let (cache, url, folders) = setup(
            &tmp,
            &[(
                "ui/app.slint",
                r#"
export component App inherits Window {
    callback clicked /* <- TEST_ME */();
}
                "#,
            )],
            &[("src/main.rs", "fn main() { obj.invoke_clicked(); obj.on_clicked(|| {}); }\n")],
        );

        let edit = perform_rename(&cache, &folders, &url, "", "pressed");
        let by_url = edits_by_url(&edit);
        let rust_edits = by_url.get(&rs_url(&tmp, "src/main.rs")).expect("missing .rs edits");
        assert!(rust_edits.contains(&"invoke_pressed".to_string()), "{rust_edits:?}");
        assert!(rust_edits.contains(&"on_pressed".to_string()), "{rust_edits:?}");
    }

    /// Function rename: `bump` -> `add` should rewrite `invoke_bump` (no
    /// `on_` for functions).
    #[test]
    fn function_rename_rewrites_invoke_only() {
        let tmp = tempdir();
        let (cache, url, folders) = setup(
            &tmp,
            &[(
                "ui/app.slint",
                r#"
export component App inherits Window {
    public function bump /* <- TEST_ME */(by: int) -> int { by + 1 }
}
                "#,
            )],
            &[("src/main.rs", "fn main() { let _ = obj.invoke_bump(2); }\n")],
        );

        let edit = perform_rename(&cache, &folders, &url, "", "add");
        let by_url = edits_by_url(&edit);
        let rust_edits = by_url.get(&rs_url(&tmp, "src/main.rs")).expect("missing .rs edits");
        assert_eq!(rust_edits, &vec!["invoke_add".to_string()]);
    }

    /// Private property: classifier returns None, scanner is skipped, only
    /// the .slint edit is produced.
    #[test]
    fn private_property_does_not_touch_host_files() {
        let tmp = tempdir();
        let (cache, url, folders) = setup(
            &tmp,
            &[(
                "ui/app.slint",
                r#"
export component App inherits Window {
    private property <int> secret /* <- TEST_ME */;
}
                "#,
            )],
            &[(
                "src/main.rs",
                // Even though the text 'get_secret' appears, it shouldn't
                // be rewritten because the slint property is private.
                "fn main() { let _ = obj.get_secret(); }\n",
            )],
        );

        let edit = perform_rename(&cache, &folders, &url, "", "hidden");
        let by_url = edits_by_url(&edit);
        assert!(
            !by_url.contains_key(&rs_url(&tmp, "src/main.rs")),
            "private property must not produce .rs edits"
        );
    }

    /// Kebab/snake normalization no-op (`my-count` -> `my_count` both
    /// produce `get_my_count`): the scanner must NOT run, otherwise the
    /// preview would show every .rs file as "modified" with identical text.
    #[test]
    fn noop_normalization_skips_scanner() {
        let tmp = tempdir();
        let (cache, url, folders) = setup(
            &tmp,
            &[(
                "ui/app.slint",
                r#"
export component App inherits Window {
    in property <int> my-count /* <- TEST_ME */;
}
                "#,
            )],
            &[("src/main.rs", "fn main() { let _ = obj.get_my_count(); }\n")],
        );

        let edit = perform_rename(&cache, &folders, &url, "", "my_count");
        let by_url = edits_by_url(&edit);
        assert!(
            !by_url.contains_key(&rs_url(&tmp, "src/main.rs")),
            "no-op normalization must skip host-language scanning"
        );
    }

    /// Cross-file inheritance: property declared in a non-exported base in
    /// `base.slint`, inherited by an exported component in `app.slint`. The
    /// .rs file calls accessors on the derived component. Renaming the base
    /// declaration must update both .slint files AND the .rs file.
    #[test]
    fn cross_file_inheritance_rewrites_all_three_files() {
        let tmp = tempdir();
        // base.slint must be loaded into the cache BEFORE app.slint, since
        // app.slint imports it. setup() loads files in array order.
        let (cache, _, folders) = setup(
            &tmp,
            &[
                (
                    "ui/base.slint",
                    r#"
export component Base {
    in property <int> shared /* <- TEST_ME */;
}
                    "#,
                ),
                (
                    "ui/app.slint",
                    r#"
import { Base } from "base.slint";
export component App inherits Base { }
                    "#,
                ),
            ],
            &[("src/main.rs", "fn main() { obj.set_shared(42); }\n")],
        );

        // The renamed declaration is in base.slint, so we drive the rename
        // from base.slint's URL.
        let base_url = slint_url(&tmp, "ui/base.slint");
        let edit = perform_rename(&cache, &folders, &base_url, "", "common");
        let by_url = edits_by_url(&edit);
        let rust_edits = by_url.get(&rs_url(&tmp, "src/main.rs")).expect("missing .rs edits");
        assert_eq!(rust_edits, &vec!["set_common".to_string()]);
    }

    /// Globals: `export global Settings { in property <int> volume; }` plus
    /// `Settings.volume` accessed via `global<Settings>().get_volume()` on
    /// the Rust side. Renaming `volume` must rewrite the .rs call site.
    #[test]
    fn global_rename_rewrites_rust_accessors() {
        let tmp = tempdir();
        let (cache, url, folders) = setup(
            &tmp,
            &[(
                "ui/app.slint",
                r#"
export global Settings {
    in-out property <int> volume /* <- TEST_ME */;
}
export component App inherits Window { }
                "#,
            )],
            &[("src/main.rs", "fn main() { app.global::<Settings>().set_volume(5); }\n")],
        );

        let edit = perform_rename(&cache, &folders, &url, "", "level");
        let by_url = edits_by_url(&edit);
        let rust_edits = by_url.get(&rs_url(&tmp, "src/main.rs")).expect("missing .rs edits");
        assert!(rust_edits.contains(&"set_level".to_string()), "{rust_edits:?}");
    }

    /// Property rename should rewrite the same `get_<n>`/`set_<n>` accessors
    /// in a `.cpp` source file -- the scanner is language-agnostic at the
    /// byte level and both backends emit identical accessor names.
    #[test]
    fn cpp_property_rename_rewrites_slint_and_cpp() {
        let tmp = tempdir();
        let (cache, url, folders) = setup(
            &tmp,
            &[(
                "ui/app.slint",
                r#"
export component App inherits Window {
    in property <int> count /* <- TEST_ME */;
}
                "#,
            )],
            &[(
                "src/main.cpp",
                "int main() { auto v = app->get_count(); app->set_count(v + 1); }\n",
            )],
        );

        let edit = perform_rename(&cache, &folders, &url, "", "total");
        let by_url = edits_by_url(&edit);
        let cpp = Url::from_file_path(tmp.path().join("src/main.cpp")).unwrap();
        let cpp_edits = by_url.get(&cpp).expect("missing .cpp edits");
        assert!(cpp_edits.contains(&"get_total".to_string()), "{cpp_edits:?}");
        assert!(cpp_edits.contains(&"set_total".to_string()), "{cpp_edits:?}");
    }

    /// Header files (`.h`, `.hpp`) are also walked. A caller that holds a
    /// reference to the generated component in a header method must be
    /// rewritten too.
    #[test]
    fn cpp_header_call_site_is_rewritten() {
        let tmp = tempdir();
        let (cache, url, folders) = setup(
            &tmp,
            &[(
                "ui/app.slint",
                r#"
export component App inherits Window {
    callback clicked /* <- TEST_ME */();
}
                "#,
            )],
            &[("include/binding.hpp", "inline void wire(App* app) { app->on_clicked([](){}); }\n")],
        );

        let edit = perform_rename(&cache, &folders, &url, "", "pressed");
        let by_url = edits_by_url(&edit);
        let hpp = Url::from_file_path(tmp.path().join("include/binding.hpp")).unwrap();
        let hpp_edits = by_url.get(&hpp).expect("missing .hpp edits");
        assert_eq!(hpp_edits, &vec!["on_pressed".to_string()]);
    }

    /// Mixed-language workspace: a single rename must rewrite call sites in
    /// both `.rs` and `.cpp` files in a single `WorkspaceEdit`.
    #[test]
    fn mixed_rust_and_cpp_workspace_both_rewritten() {
        let tmp = tempdir();
        let (cache, url, folders) = setup(
            &tmp,
            &[(
                "ui/app.slint",
                r#"
export component App inherits Window {
    in property <int> count /* <- TEST_ME */;
}
                "#,
            )],
            &[
                ("rust/src/main.rs", "fn main() { obj.get_count(); }\n"),
                ("cpp/src/main.cpp", "int main() { app->set_count(1); }\n"),
            ],
        );

        let edit = perform_rename(&cache, &folders, &url, "", "total");
        let by_url = edits_by_url(&edit);
        let rs = Url::from_file_path(tmp.path().join("rust/src/main.rs")).unwrap();
        let cpp = Url::from_file_path(tmp.path().join("cpp/src/main.cpp")).unwrap();
        assert_eq!(by_url.get(&rs).map(Vec::as_slice), Some(&["get_total".to_string()][..]));
        assert_eq!(by_url.get(&cpp).map(Vec::as_slice), Some(&["set_total".to_string()][..]));
    }
}
