Compare commits

...

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
24911dea95 fix(New+): defer $PARENT_FOLDER_NAME resolution until after user renames folder
When a template folder is created via New+, child files containing
$PARENT_FOLDER_NAME were resolved immediately using the template's
original name (e.g. "ITEM") before the user had a chance to rename
the folder. This caused them to pick up the template name rather
than the user-given name (e.g. "My Car").

Fix: in copy_template, instead of calling
resolve_variables_in_filename_and_rename_files immediately, spin
up a background thread that uses ReadDirectoryChangesW to watch the
parent directory for the folder-rename event. The watcher is started
before enter_rename_mode so no event is missed. When the user
completes the rename (or a 30-second timeout elapses),
resolve_variables_in_filename_and_rename_files is called with the
final path, so $PARENT_FOLDER_NAME resolves correctly. On Escape /
timeout the original template-derived name is used as a fallback.

Resolves: New+ Parent Folder Name is the TEMPLATE Parent Folder name
issue

Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/d406c3a2-7fbf-44b4-9751-dca5fdea04ff

Co-authored-by: MuyuanMS <116717757+MuyuanMS@users.noreply.github.com>
2026-04-29 10:15:16 +00:00
copilot-swe-agent[bot]
fd3d8eede7 Initial plan 2026-04-29 08:50:09 +00:00
3 changed files with 111 additions and 2 deletions

View File

@@ -14,6 +14,7 @@
#include <shellapi.h>
#include <Windows.h>
#include <shlobj.h>
#include <thread>
#include <vector>
#include <system_error>
#include <memory>

View File

@@ -334,6 +334,105 @@ namespace newplus::utilities
}
}
// Watches for the user renaming a newly-created folder, then resolves $PARENT_FOLDER_NAME
// in its child files using the final (user-given) name.
// If no rename is detected within timeout_ms, resolves using the original folder name.
// Must be called from a background thread; runs until the rename is detected or timeout expires.
inline void watch_for_rename_then_resolve_variables(
const std::filesystem::path& parent_dir,
const std::wstring& original_folder_name,
const DWORD timeout_ms = 30000)
{
std::filesystem::path final_target_path = parent_dir / original_folder_name;
HANDLE dir_handle = CreateFileW(
parent_dir.c_str(),
FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
nullptr);
if (dir_handle != INVALID_HANDLE_VALUE)
{
HANDLE event_handle = CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (event_handle)
{
OVERLAPPED overlapped = {};
overlapped.hEvent = event_handle;
std::vector<BYTE> buffer(32768, 0);
if (ReadDirectoryChangesW(
dir_handle,
buffer.data(),
static_cast<DWORD>(buffer.size()),
FALSE,
FILE_NOTIFY_CHANGE_DIR_NAME,
nullptr,
&overlapped,
nullptr))
{
const DWORD wait_result = WaitForSingleObject(event_handle, timeout_ms);
if (wait_result == WAIT_TIMEOUT)
{
CancelIo(dir_handle);
}
// Wait for the I/O to fully complete (either normally or after cancellation)
// before touching the OVERLAPPED or closing handles.
DWORD bytes_returned = 0;
const bool io_succeeded = GetOverlappedResult(dir_handle, &overlapped, &bytes_returned, TRUE) != FALSE;
if (wait_result == WAIT_OBJECT_0 && io_succeeded && bytes_returned > 0)
{
auto* notify_info = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(buffer.data());
bool found_old_name = false;
while (notify_info)
{
const std::wstring filename(
notify_info->FileName,
notify_info->FileNameLength / sizeof(wchar_t));
if (!found_old_name &&
notify_info->Action == FILE_ACTION_RENAMED_OLD_NAME &&
wstring_same_when_comparing_ignore_case(filename, original_folder_name))
{
found_old_name = true;
}
else if (found_old_name && notify_info->Action == FILE_ACTION_RENAMED_NEW_NAME)
{
final_target_path = parent_dir / filename;
break;
}
if (notify_info->NextEntryOffset == 0)
break;
notify_info = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(
reinterpret_cast<BYTE*>(notify_info) + notify_info->NextEntryOffset);
}
}
}
CloseHandle(event_handle);
CloseHandle(dir_handle);
}
else
{
CloseHandle(dir_handle);
}
}
// Resolve $PARENT_FOLDER_NAME in child files using the final folder name
if (std::filesystem::exists(final_target_path) && std::filesystem::is_directory(final_target_path))
{
helpers::variables::resolve_variables_in_filename_and_rename_files(final_target_path);
}
}
inline HRESULT copy_template(const template_item* template_entry, const ComPtr<IUnknown> site_of_folder)
{
HRESULT hr = S_OK;
@@ -376,10 +475,18 @@ namespace newplus::utilities
// Finally copy file/folder/subfolders
std::filesystem::path target_final_fullpath = template_entry->copy_object_to(GetActiveWindow(), target_fullpath);
// Resolve variables and rename files in newly copied folders and subfolders and files
// Resolve variables and rename files in newly copied folders and subfolders.
// For directory templates the top-level folder name is only known after the user
// completes the rename-on-creation prompt, so defer resolution until then so that
// $PARENT_FOLDER_NAME picks up the user-given name rather than the template name.
// The watcher must be started before enter_rename_mode so the rename event is not missed.
if (utilities::get_newplus_setting_resolve_variables() && helpers::filesystem::is_directory(target_final_fullpath))
{
helpers::variables::resolve_variables_in_filename_and_rename_files(target_final_fullpath);
auto parent_dir = target_final_fullpath.parent_path();
auto original_folder_name = target_final_fullpath.filename().wstring();
std::thread([parent_dir = std::move(parent_dir), original_folder_name = std::move(original_folder_name)]() {
watch_for_rename_then_resolve_variables(parent_dir, original_folder_name);
}).detach();
}
// Touch all files and set last modified to "now"

View File

@@ -15,6 +15,7 @@
#include <Windows.h>
#include <shlobj.h>
#include <algorithm>
#include <thread>
#include <vector>
#include <system_error>
#include <memory>