📖

意見ほしい。革新的テキスト編集ソフトの設計について

に公開

こんにちは。

テキストエディタが使いにくいので、自作しようと考えました。

仕様は以下の通り。

置換によって文字を編集する。
編集するファイルを指定でき、一度設定すると、IDによって指定できる。
1番と3番のような指定ができるので、楽である。

このエディタは、数百行にも及ぶソースコードの中にある、特定の要素を探し、その中に新しいテキストを書く。という作業が嫌なので制作する。

そのため、ピンポイントで編集できる置換編集という手法を採用している。

置換文字列と、置換後文字列の設定は、それ専用のフォルダを作り、その中に.form拡張子と、.to拡張子を作成し、それを参照することで置換させる。

最終的には、テキストファイルを少し編集した後、ソフトを実行すると、即変換されるようなシステムが欲しい。

そうすると、一つのフォルダで置換の設定を記述する方法は現実的でないことがわかるので、
これは要検討である。

C# VISUALSTADIOのコンソールアプリとして実装したので、
下記に張り付けておく。

意見等いただけるとうれしい。

置換の設定が書かれているフォルダがあり、その中に、form と to
拡張子のファイルが存在し、ファイル名が同じファイル同士を一つのペアとして認識し、これを変換の設定として読み込む。

その後、変換ファイルの読み込みをし、

変換するファイルを指定すると、変換が完了する。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Linq;

class ReplacementTool
{
    private static Dictionary<string, (string from, string to)> replacementRules = new Dictionary<string, (string from, string to)>();
    private static List<string> filePaths = new List<string>();
    private static string ConfigFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");
    private static string LastFolderPath = string.Empty; // 最後に設定したフォルダのパス
    private static string FromExtension = ".from"; // 変換元の拡張子
    private static string ToExtension = ".to"; // 変換先の拡張子

    // 設定を保存するための明示的なクラス
    private class Config
    {
        public List<string> FilePaths { get; set; }
        public Dictionary<string, List<string>> ReplacementRules { get; set; } // fromとtoを保存するため、Listに変更
        public string LastFolderPath { get; set; }
        public string FromExtension { get; set; }
        public string ToExtension { get; set; }
    }

    static void Main(string[] args)
    {
        LoadConfig(); // 設定を読み込む

        // アプリ起動時に前回のフォルダを参照してルールを取得
        if (!string.IsNullOrEmpty(LastFolderPath))
        {
            Console.WriteLine($"前回のフォルダを参照中: {LastFolderPath}");
            LoadReplacementRulesFromFolder(LastFolderPath);
        }

        while (true)
        {
            Console.WriteLine("\nモードを選択してください:");
            Console.WriteLine("1: 置換モード");
            Console.WriteLine("2: 置換ファイルを設定するモード");
            Console.WriteLine("3: 置換ルールを設定するモード");
            Console.WriteLine("4: 拡張子設定モード");
            Console.WriteLine("5: 終了");
            string mode = Console.ReadLine();

            switch (mode)
            {
                case "1":
                    ReplaceMode();
                    break;
                case "2":
                    ConfigureFileMode();
                    break;
                case "3":
                    ConfigureReplacementRulesMode();
                    break;
                case "4":
                    ConfigureExtensionsMode();
                    break;
                case "5":
                    SaveConfig(); // 設定を保存して終了
                    return;
                default:
                    Console.WriteLine("無効な選択です。");
                    break;
            }
        }
    }

    static void ReplaceMode()
    {
        if (filePaths.Count == 0)
        {
            Console.WriteLine("置換するファイルが設定されていません。");
            return;
        }

        if (replacementRules.Count == 0)
        {
            Console.WriteLine("置換ルールが設定されていません。");
            return;
        }

        Console.WriteLine("置換するファイルを選択してください (カンマ区切りで複数指定可能):");
        for (int i = 0; i < filePaths.Count; i++)
        {
            Console.WriteLine($"{i + 1}: {filePaths[i]}");
        }

        string input = Console.ReadLine();
        var selectedIndices = new List<int>();

        foreach (var part in input.Split(','))
        {
            if (int.TryParse(part.Trim(), out int index) && index > 0 && index <= filePaths.Count)
            {
                selectedIndices.Add(index - 1);
            }
            else
            {
                Console.WriteLine($"無効な選択: {part}");
                return;
            }
        }

        foreach (int index in selectedIndices)
        {
            string filePath = filePaths[index];
            try
            {
                string content = File.ReadAllText(filePath, Encoding.UTF8);
                string output = content;

                foreach (var rule in replacementRules)
                {
                    output = output.Replace(rule.Value.from, rule.Value.to);
                }

                File.WriteAllText(filePath, output, Encoding.UTF8);
                Console.WriteLine($"置換が完了しました: {filePath}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"エラーが発生しました ({filePath}): {ex.Message}");
            }
        }
    }

    static void ConfigureFileMode()
    {
        while (true)
        {
            Console.WriteLine("\n置換ファイルを設定するモード:");
            Console.WriteLine("1: 置換ファイル一覧を表示");
            Console.WriteLine("2: 置換ファイルを追加");
            Console.WriteLine("3: 置換ファイルを削除");
            Console.WriteLine("4: 戻る");
            string choice = Console.ReadLine();

            switch (choice)
            {
                case "1":
                    ListFilePaths();
                    break;
                case "2":
                    AddFilePath();
                    break;
                case "3":
                    RemoveFilePath();
                    break;
                case "4":
                    try
                    {
                        SaveConfig(); // 変更を保存
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"設定の保存中にエラーが発生しました: {ex.Message}");
                    }
                    return;
                default:
                    Console.WriteLine("無効な選択です。");
                    break;
            }
        }
    }

    static void ConfigureReplacementRulesMode()
    {
        while (true)
        {
            Console.WriteLine("\n置換ルールを設定するモード:");
            Console.WriteLine("1: 置換ルール一覧を表示");
            Console.WriteLine("2: フォルダから置換ルールを読み込む");
            Console.WriteLine("3: 戻る");
            string choice = Console.ReadLine();

            switch (choice)
            {
                case "1":
                    ListReplacementRules();
                    break;
                case "2":
                    Console.WriteLine("フォルダのパスを入力してください:");
                    string folderPath = Console.ReadLine().Trim('"'); // 両端の " を削除
                    folderPath = Path.GetFullPath(folderPath); // パスを正規化

                    if (!Directory.Exists(folderPath))
                    {
                        Console.WriteLine($"フォルダが見つかりません: {folderPath}");
                        Console.WriteLine("正しいパスを入力してください。");
                        continue;
                    }

                    LoadReplacementRulesFromFolder(folderPath);
                    try
                    {
                        SaveConfig(); // 変更を保存
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"設定の保存中にエラーが発生しました: {ex.Message}");
                    }
                    break;
                case "3":
                    try
                    {
                        SaveConfig(); // 変更を保存
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"設定の保存中にエラーが発生しました: {ex.Message}");
                    }
                    return;
                default:
                    Console.WriteLine("無効な選択です。");
                    break;
            }
        }
    }

    static void ConfigureExtensionsMode()
    {
        Console.WriteLine($"\n現在の拡張子設定:");
        Console.WriteLine($"変換元拡張子: {FromExtension}");
        Console.WriteLine($"変換先拡張子: {ToExtension}");
        Console.WriteLine("\n拡張子を変更しますか? (y/n)");

        if (Console.ReadLine().ToLower() == "y")
        {
            Console.WriteLine("変換元拡張子を入力してください (例: .from):");
            string fromExt = Console.ReadLine();
            if (!fromExt.StartsWith("."))
                fromExt = "." + fromExt;

            Console.WriteLine("変換先拡張子を入力してください (例: .to):");
            string toExt = Console.ReadLine();
            if (!toExt.StartsWith("."))
                toExt = "." + toExt;

            FromExtension = fromExt;
            ToExtension = toExt;
            Console.WriteLine("拡張子設定を更新しました。");

            try
            {
                SaveConfig(); // 変更を保存
            }
            catch (Exception ex)
            {
                Console.WriteLine($"設定の保存中にエラーが発生しました: {ex.Message}");
            }
        }
    }

    static void ListFilePaths()
    {
        if (filePaths.Count == 0)
        {
            Console.WriteLine("置換ファイルはありません。");
            return;
        }

        Console.WriteLine("置換ファイル一覧:");
        for (int i = 0; i < filePaths.Count; i++)
        {
            Console.WriteLine($"{i + 1}: {filePaths[i]}");
        }
    }

    static void AddFilePath()
    {
        Console.WriteLine("追加するファイルのパスを入力してください:");
        string path = Console.ReadLine().Trim('"'); // 両端の " を削除
        path = Path.GetFullPath(path); // パスを正規化

        if (File.Exists(path))
        {
            filePaths.Add(path);
            Console.WriteLine("ファイルを追加しました。");
        }
        else
        {
            Console.WriteLine($"ファイルが見つかりません: {path}");
        }
    }

    static void RemoveFilePath()
    {
        Console.WriteLine("削除するファイルの番号を入力してください:");
        if (int.TryParse(Console.ReadLine(), out int index) && index > 0 && index <= filePaths.Count)
        {
            filePaths.RemoveAt(index - 1);
            Console.WriteLine("ファイルを削除しました。");
        }
        else
        {
            Console.WriteLine("無効な選択です。");
        }
    }

    static void ListReplacementRules()
    {
        if (replacementRules.Count == 0)
        {
            Console.WriteLine("置換ルールはありません。");
            return;
        }

        Console.WriteLine("置換ルール一覧:");
        foreach (var rule in replacementRules)
        {
            Console.WriteLine($"ルール名: {rule.Key}");
            Console.WriteLine($"  変換元: {rule.Value.from}");
            Console.WriteLine($"  変換先: {rule.Value.to}");
            Console.WriteLine();
        }
    }

    static void LoadReplacementRulesFromFolder(string folderPath)
    {
        if (!Directory.Exists(folderPath))
        {
            Console.WriteLine($"フォルダが見つかりません: {folderPath}");
            return;
        }

        var fromFiles = Directory.GetFiles(folderPath, $"*{FromExtension}");
        if (fromFiles.Length == 0)
        {
            Console.WriteLine($"{FromExtension}ファイルが見つかりません。");
            return;
        }

        // 既存のルールをクリア
        replacementRules.Clear();

        foreach (var fromFile in fromFiles)
        {
            string fileName = Path.GetFileNameWithoutExtension(fromFile);
            string toFile = Path.Combine(Path.GetDirectoryName(fromFile), fileName + ToExtension);

            if (!File.Exists(toFile))
            {
                Console.WriteLine($"警告: {fileName}の変換先ファイル({ToExtension})が見つかりません。このルールはスキップします。");
                continue;
            }

            string fromContent = File.ReadAllText(fromFile, Encoding.UTF8);
            string toContent = File.ReadAllText(toFile, Encoding.UTF8);

            replacementRules[fileName] = (fromContent, toContent);
            Console.WriteLine($"置換ルールを追加: {fileName}");
            Console.WriteLine($"  変換元: {fromContent}");
            Console.WriteLine($"  変換先: {toContent}");
        }

        LastFolderPath = folderPath; // 最後に設定したフォルダを保存
    }

    static void SaveConfig()
    {
        // Dictionary<string, (string from, string to)> を保存用の形式に変換
        var saveRules = new Dictionary<string, List<string>>();
        foreach (var rule in replacementRules)
        {
            saveRules[rule.Key] = new List<string> { rule.Value.from, rule.Value.to };
        }

        var config = new Config
        {
            FilePaths = filePaths,
            ReplacementRules = saveRules,
            LastFolderPath = LastFolderPath,
            FromExtension = FromExtension,
            ToExtension = ToExtension
        };

        string json = JsonSerializer.Serialize(config);
        File.WriteAllText(ConfigFilePath, json);
        Console.WriteLine("設定を保存しました。");
    }

    static void LoadConfig()
    {
        if (File.Exists(ConfigFilePath))
        {
            try
            {
                string json = File.ReadAllText(ConfigFilePath);
                var config = JsonSerializer.Deserialize<Config>(json);

                filePaths = config.FilePaths;

                // 保存された拡張子があれば読み込む
                if (config.FromExtension != null)
                    FromExtension = config.FromExtension;
                if (config.ToExtension != null)
                    ToExtension = config.ToExtension;

                // Dictionary<string, List<string>> から Dictionary<string, (string from, string to)> への変換
                replacementRules.Clear();
                foreach (var rule in config.ReplacementRules)
                {
                    if (rule.Value.Count >= 2)
                    {
                        replacementRules[rule.Key] = (rule.Value[0], rule.Value[1]);
                    }
                }

                LastFolderPath = config.LastFolderPath;
                Console.WriteLine("設定を読み込みました。");
                    Console.WriteLine("設定を読み込みました。");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"設定の読み込み中にエラーが発生しました: {ex.Message}");
                // エラーが発生した場合は初期設定で続行
            }
        }
        else
        {
            Console.WriteLine("設定ファイルが見つかりません。初期設定で起動します。");
        }
    }
}

Discussion