Skip to content

Comments

feat: Automatically complete the output file extension#1487

Closed
yuto-trd wants to merge 1 commit intomainfrom
feat/auto-output-extension
Closed

feat: Automatically complete the output file extension#1487
yuto-trd wants to merge 1 commit intomainfrom
feat/auto-output-extension

Conversation

@yuto-trd
Copy link
Member

@yuto-trd yuto-trd commented Feb 15, 2026

Description

Added a feature to automatically complete file extensions when entering a file name in the output dialog.

  • If a file name is entered without an extension, the extension corresponding to the selected encoder is automatically appended.
  • If the encoder does not support the entered extension, it is automatically changed to the encoder's supported extension.
  • Implemented using a CombineLatest subscription for SelectedEncoder and DestinationFile.

Breaking changes

None

Fixed issues

None

Automatically append or correct the file extension when the user enters
an output filename without an extension or with an unsupported extension.
Copilot AI review requested due to automatic review settings February 15, 2026 09:18
@github-actions
Copy link
Contributor

No TODO comments were found.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8358bc7dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

{
string? ext = encoder.SupportExtensions().FirstOrDefault();
if (ext != null)
DestinationFile.Value = file + ext;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize extension before appending file name

When the destination has no extension, this branch uses file + ext directly, which produces malformed names in common edge cases (e.g. typing video. becomes video..mp4) and also fails for encoders that return extensions without a leading dot (videomp4). Using normalized extension replacement (instead of raw concatenation) avoids generating invalid output paths during filename auto-completion.

Useful? React with 👍 / 👎.

@github-actions
Copy link
Contributor

Code Coverage

Package Line Rate Branch Rate Complexity Health
Beutl.Editor 82% 76% 870
Beutl.Extensibility 14% 16% 100
Beutl.Operators 2% 0% 120
Beutl.Language 0% 20% 943
Beutl.ProjectSystem 13% 7% 2887
Beutl.Threading 99% 89% 122
Beutl.Engine 21% 19% 13751
Beutl.Configuration 39% 19% 288
Beutl.Utilities 73% 67% 358
Beutl.Core 40% 36% 2966
Summary 25% (13308 / 54059) 24% (3680 / 15473) 22405

Minimum allowed line rate is 0%

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements automatic file extension completion in the output dialog. When a user enters a filename without an extension or with an unsupported extension, the system automatically appends or changes to the appropriate extension based on the selected encoder.

Changes:

  • Added a reactive subscription that monitors both SelectedEncoder and DestinationFile changes
  • Automatically appends the encoder's default extension when filename has no extension
  • Automatically changes to encoder's default extension when current extension is unsupported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +115 to +120
else if (!encoder.IsSupported(file))
{
string? ext = encoder.SupportExtensions().FirstOrDefault();
if (ext != null)
DestinationFile.Value = Path.ChangeExtension(file, ext);
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IsSupported method in ControllableEncodingExtension uses case-sensitive comparison (line 7 of ControllableEncodingExtension.cs: Contains without a StringComparer), but file extensions on file systems can be case-insensitive (e.g., Windows) or case-sensitive (e.g., Linux). This means a file with extension ".MP4" might not be recognized as supported when the encoder lists ".mp4".

While this is an issue with the IsSupported implementation rather than the code in this diff, it affects the behavior of this new feature. If a user enters "video.MP4", the code at line 115 will treat it as unsupported and replace it with lowercase ".mp4", which may not be the user's intent. Consider whether this behavior is acceptable or if the extension comparison should be case-insensitive.

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +119
DestinationFile.Value = file + ext;
}
else if (!encoder.IsSupported(file))
{
string? ext = encoder.SupportExtensions().FirstOrDefault();
if (ext != null)
DestinationFile.Value = Path.ChangeExtension(file, ext);
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When adding an extension on line 113, the code concatenates the extension directly to the filename. However, when changing an extension on line 119, it uses Path.ChangeExtension. These two approaches may handle edge cases differently. For consistency and robustness, consider using Path.ChangeExtension in both cases, or ensure both approaches handle all edge cases identically (e.g., filenames ending with dots, or containing multiple dots).

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +122
SelectedEncoder
.CombineLatest(DestinationFile)
.Subscribe(tuple =>
{
var (encoder, file) = tuple;
if (encoder == null || string.IsNullOrEmpty(file)) return;

string currentExt = Path.GetExtension(file);
if (string.IsNullOrEmpty(currentExt))
{
string? ext = encoder.SupportExtensions().FirstOrDefault();
if (ext != null)
DestinationFile.Value = file + ext;
}
else if (!encoder.IsSupported(file))
{
string? ext = encoder.SupportExtensions().FirstOrDefault();
if (ext != null)
DestinationFile.Value = Path.ChangeExtension(file, ext);
}
})
.DisposeWith(_disposable);
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new subscription (lines 101-122) interacts with the existing encoder filtering logic (lines 91-99). When DestinationFile changes to a file with an unsupported extension, both subscriptions fire:

  1. Lines 91-99 filter the encoder list, potentially removing the current SelectedEncoder
  2. Lines 101-122 attempt to fix the extension

This interaction could cause unpredictable behavior. For example, if the SelectedEncoder becomes null due to filtering before line 106 is evaluated, the extension won't be fixed. Consider the order of these operations and whether they should be coordinated.

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +122
SelectedEncoder
.CombineLatest(DestinationFile)
.Subscribe(tuple =>
{
var (encoder, file) = tuple;
if (encoder == null || string.IsNullOrEmpty(file)) return;

string currentExt = Path.GetExtension(file);
if (string.IsNullOrEmpty(currentExt))
{
string? ext = encoder.SupportExtensions().FirstOrDefault();
if (ext != null)
DestinationFile.Value = file + ext;
}
else if (!encoder.IsSupported(file))
{
string? ext = encoder.SupportExtensions().FirstOrDefault();
if (ext != null)
DestinationFile.Value = Path.ChangeExtension(file, ext);
}
})
.DisposeWith(_disposable);
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This subscription has several critical issues:

  1. User Input Interference: The subscription triggers on every keystroke as the user types the filename. For example, if a user types "video" character by character, after typing "v", it would immediately be changed to "v.mp4", making it difficult to type the desired filename.

  2. Re-entrancy Risk: Setting DestinationFile.Value inside a subscription that observes DestinationFile creates a re-entrancy pattern. While the logic appears to terminate in most cases, this pattern is fragile and could cause infinite loops if the encoder's IsSupported method or SupportExtensions behave unexpectedly.

Consider these alternatives:

  • Use a debouncing mechanism (Throttle/Debounce operators) to only trigger after the user stops typing
  • Apply extension logic only on specific events (e.g., dialog confirmation, TextBox LostFocus)
  • Add a guard flag to prevent re-entry when modifying DestinationFile programmatically
  • Use DistinctUntilChanged() to avoid unnecessary re-triggering

Copilot uses AI. Check for mistakes.
@yuto-trd yuto-trd changed the title feat: 出力ファイルの拡張子を自動補完する feat: Automatically complete the output file extension Feb 15, 2026
@yuto-trd yuto-trd closed this Feb 15, 2026
@yuto-trd yuto-trd deleted the feat/auto-output-extension branch February 15, 2026 10:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant