AI スクリプトエディター

AIスクリプトヘルプエディターって、どんなもの?

AIスクリプトヘルプエディターは、一言でいうと**「AIに指示を出すのが、もっとラクに、もっと楽しくなるツール」**です!

普段、AIに何かお願いするときって、文章を考えたり、何度も同じことを入力したり、ちょっと手間だと感じたことはありませんか? このエディターは、そんな「めんどくさい」を解消して、あなたがAIともっとスムーズにスクリプト作成できるようにサポートしてくれます。

AIって、私たちの指示次第でいろんなすごいことをしてくれますよね。でも、「どう指示したら一番いいの?」って、結構悩みがちだったりします。このエディターは、そんなあなたの「もっとこうだったらいいのに!」に応えるために生まれました。

  • 「また同じこと書くの?」をなくす!: AIに「こういう感じで書いてね」とか「この形式で出力してね」って、繰り返し伝えることってありますよね。そういうよく使うフレーズを、このエディターが覚えてくれるんです。もう何度も入力する手間はいりません。
  • AIとの会話をスムーズに!: もしあなたがチームでAIを使っているなら、「あの人、どんな指示出してるんだろう?」って思ったことありませんか?このエディターがあれば、みんなで使える「指示の型」(テンプレート)を作れるので、AIからの返事も安定して、もっと効率的に作業を進められますよ。
  • あなたの時間を大切に!: 毎回イチから文章を考えるのって、実はすごく時間を使ってるんです。このエディターが代わりに「型」を用意してくれるので、あなたは本当に伝えたいことに集中できます。その分、他の大事な作業に時間を使えるようになりますよ。

つまり、このエディターは、あなたがAIとの「お仕事」をもっと快適に、そしてスマートに進めるための頼れる相棒なんです!

AIスクリプトヘルプエディター:ZIPからの起動と使い方

ダウンロードしたZIPファイルから、AIスクリプトヘルプエディターを素早く使うための手順です。

このアプリケーションはPythonで動作します。お使いのPCにPythonがインストールされている必要があります。

1. ZIPファイルを解凍

ダウンロードしたZIPファイルを右クリック(Macならダブルクリック)して解凍してください。中身のファイル(例: _editor_app_v4.pysettings.inicode_template.txt など)はすべて同じフォルダに置いておきましょう。

2. エディターを起動

解凍したフォルダ内の _editor_app_v4.py を実行します。

  • Windows: ファイルをダブルクリック
  • Mac/Linux: ターミナルでフォルダへ移動し、python _editor_app_v5.py を実行。

3. 基本的な使い方

  • 左側: 定型指示リスト。ダブルクリックで右のエディターに追記。
  • 右側: メインエディター。AIへの指示を記述。
  • 上部メニュー:
    • 「ファイル」: 新規作成、開く、保存など。
    • 「テンプレート選択」: AIの目的に合わせたテンプレートを切り替え(内容と定型文が連動)。
    • 「日付ファイルで保存」: 日付とテンプレート名入りのファイルで保存でき、履歴管理が楽になります。

これで、AIプロンプト作成がよりスムーズになりますよ!

どんな風に使うの?

使い方はとってもシンプル!主に3つの機能が、あなたのAI活用をサポートしてくれます。

1. テンプレートでサッとスタート!

AIに何かお願いするときって、「文章作ってほしいな」「画像生成してほしいな」「この書類、校正してほしいな」みたいに、目的が決まっていることが多いですよね。このエディターには、そんな目的ごとにあらかじめ「型」が用意されています。

メニューから「コード作成」とか「文書校正」とか、やりたいことに合ったテンプレートを選ぶだけで、エディターに基本的な指示のひな形がパッと表示されます。これで「何から書こう?」って迷う時間がグッと減りますよ。

しかも、前回使ったテンプレートを覚えてくれるので、次にエディターを開いたときも、すぐに前回の続きから始められます!もちろん、あなただけのオリジナルテンプレートを作ることも可能です。

2. 定型文でポンと追加!

テンプレートを選んだら、次に活躍するのが定型文です。エディターの左側に、そのテンプレートに合わせた「よく使うフレーズ集」が表示されます。

例えば、コード作成のテンプレートなら、「Pythonで書いてね」「エラーハンドリングもお願い」といった、プログラミングでよく使う指示が並んでいるイメージです。使いたいフレーズをダブルクリックするだけで、エディターにサッと追加できます。キーボードを打つ手間が省けて、ストレスフリー!

「この表現、また使うかも!」と思ったら、エディターに書いた文章から、一行を選んで定型文に追加することもできます。どんどん自分だけの便利フレーズ集を育てていけますよ。

3. 日付ファイルで記録を残そう!

AIとのやり取りって、後から見返すと「あの時の指示、よかったな」とか「これは改善しよう」といった発見がありますよね。このエディターでは、あなたが作成した指示やAIからの応答を、分かりやすいファイル名で保存できます。

例えば、「20250704_文書校正.txt」のように、日付と使ったテンプレートの名前が自動でファイル名になるんです。時間を加えて「20250704_2315_文書校正.txt」と、より細かく記録することも可能!

保存先も、このエディターがあるフォルダか、あなたの「マイドキュメント」フォルダかを選べます。こうすることで、AIとどんなやり取りをしたのかを一目で把握できて、あなたのAI活用がどんどん賢くなっていきますよ。

スクリプト

import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, Toplevel, StringVar, BooleanVar
import os
import subprocess
import sys
import configparser
import datetime
import webbrowser

# --- Constants ---
SETTINGS_FILE = "settings.ini"
DEFAULT_SETTINGS_CONTENT = """
[Settings]
last_template = 文書校正
# last_file_path =
# last_scri_file =

[Templates]
コード作成 = code_template.txt,_SCRI_code.txt
画像作成 = image_template.txt,_SCRI_image.txt
文書校正 = document_template.txt,_SCRI_document.txt
調査調査 = research_template.txt,_SCRI_research.txt

[DateSaveOptions]
save_to_my_documents_default = False
include_time_default = False
menu_label_script_folder_date_only = スクリプトフォルダに保存 (YYYYMMDD_テンプレ名.txt)
menu_label_script_folder_date_time = スクリプトフォルダに保存 (YYYYMMDD_HHMM_テンプレ名.txt)
menu_label_my_documents_date_only = マイドキュメントに保存 (YYYYMMDD_テンプレ名.txt)
menu_label_my_documents_date_time = マイドキュメントに保存 (YYYYMMDD_HHMM_テンプレ名.txt)
"""

class ScriptHelperEditor:
    """
    AIへのスクリプト作成を補助する機能を備えたテキストエディタ。
    エディタ枠外に行数表示機能、テンプレート選択・記憶機能を追加。
    """

    def __init__(self, root):
        self.root = root
        self.current_file_path = None # 現在開いているファイルのパス
        self.current_template = None # 現在選択中のテンプレート名
        self.current_scri_file = None # 現在のテンプレートに紐づく定型文ファイルのパス
        self.config = configparser.ConfigParser()

        # Search/Replace related variables
        self.search_toplevel = None
        self.replace_toplevel = None
        self.find_start_index = "1.0"
        self.last_search_term = None
        self.case_sensitive = BooleanVar(value=False)


        # Load or create settings.ini
        self._load_or_create_settings()

        # Initialize template and scri file mappings from config
        self.TEMPLATE_MAP = {}
        self.SCRI_TEMPLATE_MAP = {}
        if self.config.has_section('Templates'):
            for name, paths in self.config.items('Templates'):
                try:
                    editor_file, scri_file = paths.split(',')
                    self.TEMPLATE_MAP[name.strip()] = editor_file.strip()
                    self.SCRI_TEMPLATE_MAP[name.strip()] = scri_file.strip()
                except ValueError:
                    messagebox.showwarning("設定エラー", f"settings.iniの[Templates]セクションの設定が不正です: {name} = {paths}")
                    continue

        # Default date save options and menu labels
        self.save_to_my_documents_default = self.config.getboolean('DateSaveOptions', 'save_to_my_documents_default', fallback=False)
        self.include_time_default = self.config.getboolean('DateSaveOptions', 'include_time_default', fallback=False)
        self.menu_label_script_folder_date_only = self.config.get('DateSaveOptions', 'menu_label_script_folder_date_only', fallback="スクリプトフォルダに保存 (YYYYMMDD_テンプレ名.txt)")
        self.menu_label_script_folder_date_time = self.config.get('DateSaveOptions', 'menu_label_script_folder_date_time', fallback="スクリプトフォルダに保存 (YYYYMMDD_HHMM_テンプレ名.txt)")
        self.menu_label_my_documents_date_only = self.config.get('DateSaveOptions', 'menu_label_my_documents_date_only', fallback="マイドキュメントに保存 (YYYYMMDD_テンプレ名.txt)")
        self.menu_label_my_documents_date_time = self.config.get('DateSaveOptions', 'menu_label_my_documents_date_time', fallback="マイドキュメントに保存 (YYYYMMDD_HHMM_テンプレ名.txt)")


        self.root.protocol("WM_DELETE_WINDOW", self.exit_editor)

        self.root.title("AI Script Helper Editor")
        self.root.geometry("1100x750")

        # Main frame
        main_frame = tk.Frame(self.root)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # Left frame (Fixed phrases area)
        left_frame = tk.Frame(main_frame, width=350)
        left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
        left_frame.pack_propagate(False)

        # Right frame (Text editor area)
        right_frame = tk.Frame(main_frame)
        right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)

        # --- Left: Fixed Phrases Display Area ---
        self.scri_label_text = tk.StringVar() # To dynamically update label text
        self.scri_label = tk.Label(left_frame, textvariable=self.scri_label_text, font=("Helvetica", 12, "bold"))
        self.scri_label.pack(pady=(0, 5), anchor="w")

        info_label = tk.Label(left_frame, text="ダブルクリックでエディタに追記", fg="blue")
        info_label.pack(pady=(0, 10), anchor="w")

        list_frame = tk.Frame(left_frame)
        list_frame.pack(fill=tk.BOTH, expand=True)

        self.scri_listbox = tk.Listbox(list_frame, font=("Meiryo UI", 10))
        self.scri_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        scrollbar = tk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.scri_listbox.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.scri_listbox.config(yscrollcommand=scrollbar.set)

        self.scri_listbox.bind("<Double-1>", self.add_scri_to_editor)
        self.scri_listbox.bind("<Button-3>", self.show_scri_context_menu) # Right-click for listbox

        # 定型文リストボックスの操作ボタン
        scri_buttons_frame = tk.Frame(left_frame)
        scri_buttons_frame.pack(fill=tk.X, pady=(5, 0))

        move_up_button = tk.Button(scri_buttons_frame, text="▲ 上へ", command=lambda: self.move_scri_item(-1))
        move_up_button.pack(side=tk.LEFT, expand=True, padx=(0, 2))

        move_down_button = tk.Button(scri_buttons_frame, text="▼ 下へ", command=lambda: self.move_scri_item(1))
        move_down_button.pack(side=tk.LEFT, expand=True, padx=(2, 0))


        # --- Right: Text Editor and Related Widgets ---
        # Add line to fixed phrases feature
        add_scri_frame = tk.Frame(right_frame)
        add_scri_frame.pack(fill=tk.X, pady=(0, 5))

        self.line_num_entry = tk.Entry(add_scri_frame, width=10)
        self.line_num_entry.pack(side=tk.LEFT, padx=(0, 5))

        add_button = tk.Button(add_scri_frame, text="選択テンプレートの定型文に追記", command=self.add_line_to_scri) # Updated text
        add_button.pack(side=tk.LEFT)

        # Clipboard copy button
        copy_editor_content_button = tk.Button(add_scri_frame, text="エディタ内容をコピー", command=self.copy_editor_content_to_clipboard)
        copy_editor_content_button.pack(side=tk.RIGHT, padx=(10, 0))

        # Text editor container frame
        editor_container_frame = tk.Frame(right_frame)
        editor_container_frame.pack(expand=True, fill="both")

        # Line numbers widget
        self.line_numbers = tk.Text(editor_container_frame, width=4, padx=3, takefocus=0,
                                     border=0, background="#f0f0f0", state='disabled',
                                     font=("Meiryo UI", 11))
        self.line_numbers.pack(side=tk.LEFT, fill=tk.Y)

        # Text editor itself
        self.text_editor = scrolledtext.ScrolledText(editor_container_frame, wrap=tk.WORD, undo=True, font=("Meiryo UI", 11))
        self.text_editor.pack(expand=True, fill="both", side=tk.LEFT)
        self.text_editor.bind("<Button-3>", self.show_editor_context_menu) # Right-click for editor

        # Configure tags for search highlighting
        self.text_editor.tag_configure("search", background="yellow")

        # Synchronize line numbers and text editor scrolling
        self.text_editor.vbar.config(command=self.yview_both)
        self.line_numbers.config(yscrollcommand=self.text_editor.yview)
        self.text_editor.config(yscrollcommand=self.yview_editor)

        # Bind text editor content change events
        self.text_editor.bind("<KeyRelease>", self._on_text_change)
        self.text_editor.bind("<ButtonRelease-1>", self._on_text_change)
        self.text_editor.bind("<MouseWheel>", self._on_text_change)
        self.text_editor.bind("<<Undo>>", self._on_text_change)
        self.text_editor.bind("<<Redo>>", self._on_text_change)
        self.text_editor.bind("<Control-f>", self.find_text)
        self.text_editor.bind("<Control-h>", self.replace_text)


        # --- Create Menu Bar ---
        self.create_menu()

        # --- Footer for Copyright and Link ---
        footer_frame = tk.Frame(self.root, bd=1, relief=tk.RAISED)
        footer_frame.pack(side=tk.BOTTOM, fill=tk.X)

        copyright_label = tk.Label(footer_frame, text="© sakaida.jp", font=("Arial", 9))
        copyright_label.pack(side=tk.LEFT, padx=5, pady=2)

        # Make the link clickable
        link_label = tk.Label(footer_frame, text="sakaida.jp", fg="blue", cursor="hand2", font=("Arial", 9, "underline"))
        link_label.pack(side=tk.RIGHT, padx=5, pady=2)
        link_label.bind("<Button-1>", lambda e: webbrowser.open_new("https://sakaida.jp"))

        # --- Startup processing ---
        self.load_state_from_settings() # Load settings for last session

    def _load_or_create_settings(self):
        """Loads settings from settings.ini or creates it if it doesn't exist."""
        if not os.path.exists(SETTINGS_FILE):
            try:
                with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
                    f.write(DEFAULT_SETTINGS_CONTENT.strip())
                messagebox.showinfo("設定ファイル作成", f"'{SETTINGS_FILE}' が見つからなかったため、デフォルトの設定ファイルを作成しました。")
            except Exception as e:
                messagebox.showerror("エラー", f"設定ファイルの作成に失敗しました:\n{e}")
                sys.exit(1) # Critical error, exit application

        try:
            self.config.read(SETTINGS_FILE, encoding='utf-8')
        except Exception as e:
            messagebox.showerror("設定ファイル読み込みエラー", f"'{SETTINGS_FILE}' の読み込み中にエラーが発生しました:\n{e}\n\n"
                                                               "ファイルが破損している可能性があります。削除して再起動してください。")
            sys.exit(1)


    def create_menu(self):
        """Creates the menu bar."""
        menubar = tk.Menu(self.root)
        self.root.config(menu=menubar)

        # ファイルメニュー
        file_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="ファイル", menu=file_menu)
        file_menu.add_command(label="新規作成", command=self.new_file)
        file_menu.add_command(label="開く", command=self.open_file)
        file_menu.add_command(label="上書き保存", command=self.save_file)
        file_menu.add_command(label="名前を付けて保存", command=self.save_as_file)
        file_menu.add_separator()
        file_menu.add_command(label="終了", command=self.exit_editor)

        # 編集メニュー
        edit_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="編集", menu=edit_menu)
        edit_menu.add_command(label="元に戻す", command=self.text_editor.edit_undo)
        edit_menu.add_command(label="やり直し", command=self.text_editor.edit_redo)
        edit_menu.add_separator()
        edit_menu.add_command(label="切り取り", command=lambda: self.text_editor.event_generate("<<Cut>>"))
        edit_menu.add_command(label="コピー", command=lambda: self.text_editor.event_generate("<<Copy>>"))
        edit_menu.add_command(label="貼り付け", command=lambda: self.text_editor.event_generate("<<Paste>>"))
        edit_menu.add_separator()
        edit_menu.add_command(label="すべて選択", command=lambda: self.text_editor.tag_add("sel", "1.0", "end"))
        edit_menu.add_separator()
        edit_menu.add_command(label="検索 (Ctrl+f)", command=self.find_text)
        edit_menu.add_command(label="置き換え (Ctrl+h)", command=self.replace_text)


        # テンプレート選択メニュー (Dynamically generated)
        template_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="テンプレート選択", menu=template_menu)
        for template_name in self.TEMPLATE_MAP.keys():
            template_menu.add_command(label=template_name, command=lambda name=template_name: self.set_template(name))

        # 日付ファイルで保存メニュー
        date_save_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="日付ファイルで保存", menu=date_save_menu)

        # スクリプトと同じフォルダに保存
        date_save_menu.add_command(
            label=self.menu_label_script_folder_date_only,
            command=lambda: self.save_file_with_date_and_template(include_time=False, to_my_documents=False)
        )
        date_save_menu.add_command(
            label=self.menu_label_script_folder_date_time,
            command=lambda: self.save_file_with_date_and_template(include_time=True, to_my_documents=False)
        )
        date_save_menu.add_separator()

        # マイドキュメントフォルダに保存
        date_save_menu.add_command(
            label=self.menu_label_my_documents_date_only,
            command=lambda: self.save_file_with_date_and_template(include_time=False, to_my_documents=True)
        )
        date_save_menu.add_command(
            label=self.menu_label_my_documents_date_time,
            command=lambda: self.save_file_with_date_and_template(include_time=True, to_my_documents=True)
        )

        # ヘルプメニューを追加
        help_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="ヘルプ", menu=help_menu)
        help_menu.add_command(label="このエディターについて", command=self.open_about_editor_link)


    def open_about_editor_link(self):
        """このエディターについてのリンクを開く"""
        webbrowser.open_new("https://sakaida.jp/ai-sc-edit/")


    def show_editor_context_menu(self, event):
        """エディタの右クリックメニューを表示する"""
        context_menu = tk.Menu(self.root, tearoff=0)

        try:
            selected_text = self.text_editor.get(tk.SEL_FIRST, tk.SEL_LAST)
        except tk.TclError:
            selected_text = "" # No text selected

        if selected_text:
            context_menu.add_command(label="選択範囲を定型文に追加", command=self.add_selected_text_to_scri)
        else:
            context_menu.add_command(label="選択範囲を定型文に追加", state=tk.DISABLED) # Disable if no selection

        context_menu.add_separator()
        context_menu.add_command(label="切り取り", command=lambda: self.text_editor.event_generate("<<Cut>>"))
        context_menu.add_command(label="コピー", command=lambda: self.text_editor.event_generate("<<Copy>>"))
        context_menu.add_command(label="貼り付け", command=lambda: self.text_editor.event_generate("<<Paste>>"))
        context_menu.add_separator()
        context_menu.add_command(label="検索", command=self.find_text)
        context_menu.add_command(label="置き換え", command=self.replace_text)

        context_menu.tk_popup(event.x_root, event.y_root)

    def show_scri_context_menu(self, event):
        """定型文リストボックスの右クリックメニューを表示する"""
        context_menu = tk.Menu(self.root, tearoff=0)

        # 右クリックされた位置のアイテムを特定
        index = self.scri_listbox.nearest(event.y)
        if index != -1:
            self.scri_listbox.selection_clear(0, tk.END)
            self.scri_listbox.selection_set(index)
            self.scri_listbox.activate(index)

            selected_text = self.scri_listbox.get(index)

            context_menu.add_command(label="エディターへ追加", command=lambda: self.add_scri_to_editor(event))
            context_menu.add_command(label="選択肢から削除", command=lambda: self.delete_scri_item(selected_text))
            context_menu.add_separator()
            context_menu.add_command(label="上に移動", command=lambda: self.move_scri_item(-1))
            context_menu.add_command(label="下に移動", command=lambda: self.move_scri_item(1))
        else:
            context_menu.add_command(label="エディターへ追加", state=tk.DISABLED)
            context_menu.add_command(label="選択肢から削除", state=tk.DISABLED)
            context_menu.add_separator()
            context_menu.add_command(label="上に移動", state=tk.DISABLED)
            context_menu.add_command(label="下に移動", state=tk.DISABLED)

        context_menu.tk_popup(event.x_root, event.y_root)

    def _update_line_numbers(self, *args):
        """Updates and displays line numbers for the text editor."""
        self.line_numbers.config(state='normal')
        self.line_numbers.delete("1.0", tk.END)

        lines = self.text_editor.get("1.0", tk.END).split('\n')

        line_count = len(lines)
        line_numbers_text = ""
        for i in range(1, line_count + 1):
            line_numbers_text += str(i) + "\n"

        self.line_numbers.insert("1.0", line_numbers_text)
        self.line_numbers.config(state='disabled')

        self.line_numbers.yview_moveto(self.text_editor.yview()[0])

    def yview_both(self, *args):
        """Scrolls both the text editor and line number display."""
        self.text_editor.yview(*args)
        self.line_numbers.yview(*args)

    def yview_editor(self, *args):
        """Command for text editor scrollbar to also scroll line numbers."""
        self.text_editor.yview(*args)
        self.line_numbers.yview(*args)

    def _on_text_change(self, event=None):
        """Called when the content of the text editor changes."""
        # Clear search highlights on any text modification
        self.text_editor.tag_remove("search", "1.0", tk.END)

        if event and event.type == "23" and (event.keysym == "Undo" or event.keysym == "Redo"):
            self._update_line_numbers()
        elif event and event.keysym in ("BackSpace", "Delete", "Return"):
            self._update_line_numbers()
        elif event and event.char and len(event.char) == 1:
            self._update_line_numbers()
        elif event and (event.num == 1): # Mouse click
             self.root.after(10, self._update_line_numbers)
        elif event and (event.num == 4 or event.num == 5): # Mouse scroll
            self.line_numbers.yview_moveto(self.text_editor.yview()[0])


    def update_scri_label(self):
        """Updates the label for the fixed phrase listbox."""
        if self.current_template:
            scri_filename = os.path.basename(self.SCRI_TEMPLATE_MAP.get(self.current_template, "ファイルなし"))
            self.scri_label_text.set(f"定型指示 ({self.current_template} - {scri_filename})")
        else:
            if self.current_scri_file:
                self.scri_label_text.set(f"定型指示 ({os.path.basename(self.current_scri_file)})")
            else:
                self.scri_label_text.set("定型指示 (選択なし)")

    def load_scri_file(self):
        """Loads the current fixed phrase file and displays it in the listbox."""
        self.scri_listbox.delete(0, tk.END)
        self.update_scri_label()

        if not self.current_scri_file:
            return

        scri_filepath = self.current_scri_file
        # if the current_scri_file is just a filename (e.g., from SCRI_TEMPLATE_MAP),
        # assume it's in the script's directory.
        if not os.path.isabs(scri_filepath):
            scri_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), scri_filepath)

        try:
            if not os.path.exists(scri_filepath):
                open(scri_filepath, 'w', encoding='utf-8').close()

            with open(scri_filepath, 'r', encoding='utf-8') as f:
                for line in f:
                    if line.strip():
                        self.scri_listbox.insert(tk.END, line.strip())
        except Exception as e:
            messagebox.showerror("エラー", f"定型文ファイル '{os.path.basename(scri_filepath)}' の読み込みに失敗しました:\n{e}")

    def append_text_to_current_scri_file(self, text_content):
        """汎用的にテキストを現在の定型文ファイルに追記する"""
        if not self.current_scri_file:
            messagebox.showwarning("警告", "定型文を追加するファイルが特定できません。")
            return False

        if not text_content.strip():
            messagebox.showinfo("情報", "追加する内容が空です。")
            return False

        scri_filepath = self.current_scri_file
        if not os.path.isabs(scri_filepath):
            scri_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), scri_filepath)

        try:
            # 追記ではなく、リストボックスから直接ファイルに保存する形に変更
            # appendは行わない。一旦全てリストボックスに読み込み、リストボックスの変更をファイルに反映させる
            # ここでは単にリストボックスに要素を追加するのみ
            self.scri_listbox.insert(tk.END, text_content.strip())
            messagebox.showinfo("成功", f"'{text_content[:30].strip()}...' を定型文リストに追加しました。")
            self.save_scri_file_from_listbox() # リストボックスの内容をファイルに保存
            return True
        except Exception as e:
            messagebox.showerror("エラー", f"定型文リストへの追加に失敗しました:\n{e}")
            return False

    def add_line_to_scri(self):
        """エディタの指定行を現在の定型文ファイルに追記する"""
        try:
            line_num_str = self.line_num_entry.get()
            if not line_num_str.isdigit():
                messagebox.showwarning("入力エラー", "有効な行番号を数字で入力してください。")
                return

            line_num = int(line_num_str)
            line_content = self.text_editor.get(f"{line_num}.0", f"{line_num}.end").strip()

            self.append_text_to_current_scri_file(line_content)
            self.line_num_entry.delete(0, tk.END)

        except tk.TclError:
            messagebox.showerror("エラー", "指定された行番号は存在しません。")
        except Exception as e:
            messagebox.showerror("エラー", f"行の取得に失敗しました:\n{e}")

    def add_selected_text_to_scri(self):
        """エディタで選択されたテキストを現在の定型文ファイルに追記する"""
        try:
            selected_text = self.text_editor.get(tk.SEL_FIRST, tk.SEL_LAST)
            self.append_text_to_current_scri_file(selected_text)
        except tk.TclError:
            messagebox.showinfo("情報", "選択されているテキストがありません。")
        except Exception as e:
            messagebox.showerror("エラー", f"選択テキストの取得に失敗しました:\n{e}")


    def add_scri_to_editor(self, event):
        """リストボックスで選択した項目をテキストエディタに追記する (ダブルクリック/右クリック用)"""
        selected_indices = self.scri_listbox.curselection()
        if not selected_indices:
            return

        selected_text = self.scri_listbox.get(selected_indices[0])

        self.text_editor.insert(tk.INSERT, selected_text + "\n")

        original_bg = self.scri_listbox.itemcget(selected_indices[0], "background")
        self.scri_listbox.itemconfig(selected_indices[0], {'bg':'#aaddff'})
        self.root.after(200, lambda: self.scri_listbox.itemconfig(selected_indices[0], {'bg': original_bg}))

        self._update_line_numbers()

    def delete_scri_item(self, item_text):
        """定型文リストボックスから項目を削除する"""
        if not self.current_scri_file:
            messagebox.showwarning("警告", "削除対象の定型文ファイルが特定できません。")
            return

        selected_indices = self.scri_listbox.curselection()
        if not selected_indices:
            messagebox.showinfo("情報", "削除する項目を選択してください。")
            return

        # 念のため、選択されている項目が引数で渡されたitem_textと一致するか確認
        if self.scri_listbox.get(selected_indices[0]).strip() != item_text.strip():
            # これは右クリックメニューから呼ばれた場合のみ発生しうる
            # ボタンクリックの場合はcurselection()とitem_textが一致するはず
            messagebox.showwarning("警告", "選択された項目と削除対象の項目が一致しません。")
            return

        confirm = messagebox.askyesno(
            "削除の確認",
            f"'{item_text[:50]}...' を定型文から削除しますか?\nこの操作は元に戻せません。"
        )
        if not confirm:
            return

        self.scri_listbox.delete(selected_indices[0])
        messagebox.showinfo("成功", f"'{item_text[:30]}...' を定型文から削除しました。")
        self.save_scri_file_from_listbox() # 変更をファイルに保存

    def move_scri_item(self, direction):
        """
        定型文リストボックスで選択された項目を移動する。
        :param direction: -1 で上に移動、 1 で下に移動。
        """
        selected_indices = self.scri_listbox.curselection()
        if not selected_indices:
            return

        current_index = selected_indices[0]
        new_index = current_index + direction

        if 0 <= new_index < self.scri_listbox.size():
            item_to_move = self.scri_listbox.get(current_index)
            self.scri_listbox.delete(current_index)
            self.scri_listbox.insert(new_index, item_to_move)
            self.scri_listbox.selection_set(new_index)
            self.scri_listbox.activate(new_index)
            self.scri_listbox.see(new_index) # 移動後、アイテムが見えるようにスクロール

            self.save_scri_file_from_listbox() # 変更をファイルに保存
        else:
            messagebox.showinfo("情報", "これ以上移動できません。")

    def save_scri_file_from_listbox(self):
        """
        現在のリストボックスの内容を定型文ファイルに保存する。
        """
        if not self.current_scri_file:
            messagebox.showwarning("警告", "保存対象の定型文ファイルが特定できません。")
            return False

        scri_filepath = self.current_scri_file
        if not os.path.isabs(scri_filepath):
            scri_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), scri_filepath)

        try:
            with open(scri_filepath, 'w', encoding='utf-8') as f:
                for i in range(self.scri_listbox.size()):
                    line = self.scri_listbox.get(i).strip()
                    if line: # 空行は保存しない
                        f.write(line + '\n')
            # messagebox.showinfo("保存完了", f"定型文の順序を '{os.path.basename(scri_filepath)}' に保存しました。")
            return True
        except Exception as e:
            messagebox.showerror("エラー", f"定型文ファイルの保存に失敗しました:\n{e}")
            return False

    def copy_editor_content_to_clipboard(self):
        """Copies all content from the editor to the clipboard."""
        editor_content = self.text_editor.get("1.0", tk.END).strip()

        if not editor_content:
            messagebox.showinfo("情報", "コピーする内容がありません。")
            return

        self.root.clipboard_clear()
        self.root.clipboard_append(editor_content)
        messagebox.showinfo("コピー完了", "エディタの内容がクリップボードにコピーされました。")

    def get_template_file_paths(self, template_name):
        """Returns the file paths for editor content and scri content for a given template."""
        editor_file = self.TEMPLATE_MAP.get(template_name)
        scri_file = self.SCRI_TEMPLATE_MAP.get(template_name)
        return editor_file, scri_file

    def load_file_content(self, filepath):
        """指定されたファイルパスのコンテンツをエディタにロードするヘルパーメソッド"""
        try:
            if not os.path.exists(filepath):
                messagebox.showerror("エラー", f"ファイルが見つかりません:\n{filepath}")
                return False

            with open(filepath, 'r', encoding='utf-8') as f:
                content = f.read()

            self.text_editor.delete("1.0", tk.END)
            self.text_editor.insert("1.0", content)
            self.current_file_path = filepath

            self.current_template = None
            self.current_scri_file = None # ファイルを開いた場合は定型文ファイルもリセット
            self.load_scri_file() # 定型文リストボックスをクリアまたは更新

            self.root.title(f"{os.path.basename(filepath)} - AI Script Helper Editor")
            self._update_line_numbers()
            self.text_editor.edit_modified(False)
            return True
        except Exception as e:
            messagebox.showerror("エラー", f"ファイルの読み込みに失敗しました:\n{e}")
            return False

    def set_template(self, template_name):
        """
        Loads the specified template, replaces editor content, and sets it as the current template.
        Also switches the fixed phrase file.
        """
        if self.text_editor.edit_modified():
             response = messagebox.askyesnocancel(
                 "変更の保存",
                 f"現在のファイル {os.path.basename(self.current_file_path or '(新規ファイル)')} が変更されています。保存しますか?"
             )
             if response is True:
                 self.save_file()
             elif response is False:
                 pass
             else:
                 return

        editor_file, scri_file = self.get_template_file_paths(template_name)

        if not editor_file:
            messagebox.showerror("エラー", f"'{template_name}' に対応するエディタテンプレートファイルパスが見つかりません。settings.iniを確認してください。")
            self._reset_to_neutral_state()
            return
        if not scri_file:
            messagebox.showerror("エラー", f"'{template_name}' に対応する定型文ファイルパスが見つかりません。settings.iniを確認してください。")
            self._reset_to_neutral_state()
            return

        # Construct full paths for template files (assuming they are in the script's directory)
        editor_full_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), editor_file)
        scri_full_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), scri_file)


        try:
            # Ensure template content file exists
            if not os.path.exists(editor_full_path):
                open(editor_full_path, 'w', encoding='utf-8').close()

            with open(editor_full_path, 'r', encoding='utf-8') as f:
                content = f.read()

            self.text_editor.delete("1.0", tk.END)
            self.text_editor.insert("1.0", content)
            self.root.title(f"{template_name} - AI Script Helper Editor")
            self._update_line_numbers()
            self.text_editor.edit_modified(False)

            self.current_template = template_name
            self.current_file_path = editor_full_path # テンプレートのファイルパスを設定
            self.current_scri_file = scri_full_path # テンプレートに対応する定型指示ファイルを設定
            self.load_scri_file()

            messagebox.showinfo("テンプレート変更", f"'{template_name}' テンプレートを読み込みました。")
            self.save_state_to_settings()
        except Exception as e:
            messagebox.showerror("エラー", f"テンプレート '{template_name}' の読み込みに失敗しました:\n{e}")
            self._reset_to_neutral_state()

    def _reset_to_neutral_state(self, skip_save_check=False):
        """エディタを中立的な状態(新規ファイル状態)にリセットするヘルパーメソッド"""
        self.text_editor.delete("1.0", tk.END)
        self.current_file_path = None
        self.current_template = None
        self.current_scri_file = None
        self.root.title("新規ファイル - AI Script Helper Editor")
        self._update_line_numbers()
        self.text_editor.edit_modified(False)
        self.load_scri_file() # 定型文リストボックスをクリア
        if not skip_save_check:
            self.save_state_to_settings()

    def new_file(self, skip_save_check=False):
        """Creates a new, blank file."""
        if not skip_save_check and self.text_editor.edit_modified():
             response = messagebox.askyesnocancel(
                 "変更の保存",
                 f"現在のファイル {os.path.basename(self.current_file_path or '(新規ファイル)')} が変更されています。保存しますか?"
             )
             if response is True:
                 self.save_file()
             elif response is False:
                 pass
             else:
                 return
        self._reset_to_neutral_state(skip_save_check)

    def open_file(self):
        """Opens a file from disk."""
        if self.text_editor.edit_modified():
             response = messagebox.askyesnocancel(
                 "変更の保存",
                 f"現在のファイル {os.path.basename(self.current_file_path or '(新規ファイル)')} が変更されています。保存しますか?"
             )
             if response is True:
                 self.save_file()
             elif response is False:
                 pass
             else:
                 return

        filepath = filedialog.askopenfilename(
            filetypes=[("Text Files", "*.txt"), ("Python Files", "*.py"), ("All Files", "*.*")]
        )
        if not filepath:
            return

        # ファイルを開く際はテンプレートとの紐付けを解除
        self.current_template = None
        if self.load_file_content(filepath):
            self.save_state_to_settings()

    def save_file(self):
        """Saves the current file, or prompts for save-as if it's a new file."""
        target_filepath = None
        if self.current_template:
            editor_file, _ = self.get_template_file_paths(self.current_template)
            if editor_file:
                 target_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), editor_file)
            else:
                messagebox.showerror("エラー", "テンプレートのファイルパスが不正です。名前を付けて保存します。")
                self.save_as_file()
                return

        elif self.current_file_path:
            target_filepath = self.current_file_path

        if target_filepath:
            try:
                with open(target_filepath, 'w', encoding='utf-8') as f:
                    f.write(self.text_editor.get("1.0", tk.END))
                messagebox.showinfo("成功", f"ファイルを保存しました:\n{target_filepath}")
                self.text_editor.edit_modified(False)
                self.save_state_to_settings()
            except Exception as e:
                messagebox.showerror("エラー", f"ファイルの保存に失敗しました:\n{e}")
        else:
            self.save_as_file()

    def save_as_file(self):
        """Saves the current file with a new name."""
        filepath = filedialog.asksaveasfilename(
            defaultextension=".txt",
            filetypes=[("Text Files", "*.txt"), ("Python Files", "*.py"), ("All Files", "*.*")]
        )
        if not filepath:
            return

        try:
            with open(filepath, 'w', encoding='utf-8') as f:
                f.write(self.text_editor.get("1.0", tk.END))

            self.current_file_path = filepath
            self.current_template = None # 名前を付けて保存したらテンプレートとの関連は切れる
            self.current_scri_file = None # 定型文ファイルとの関連も切れる
            self.load_scri_file() # 定型文リストボックスをクリア
            self.root.title(f"{os.path.basename(filepath)} - AI Script Helper Editor")
            messagebox.showinfo("成功", f"ファイルを保存しました:\n{self.current_file_path}")
            self.text_editor.edit_modified(False)
            self.save_state_to_settings()
        except Exception as e:
            messagebox.showerror("エラー", f"ファイルの名前を付けて保存に失敗しました:\n{e}")

    def save_file_with_date_and_template(self, include_time=None, to_my_documents=None):
        """
        現在のエディタ内容を日付とテンプレート名を含むファイル名で保存する。
        保存後、そのファイルを current_file_path として設定し、設定ファイルに保存する。
        :param include_time: Trueの場合、ファイル名に時間を含める (YYYYMMDD_HHMM)。Noneの場合はデフォルト設定を使用。
        :param to_my_documents: Trueの場合、マイドキュメントフォルダに保存。Falseの場合、スクリプトと同じフォルダに保存。Noneの場合はデフォルト設定を使用。
        """
        # Use default settings if parameters are None
        if include_time is None:
            include_time = self.include_time_default
        if to_my_documents is None:
            to_my_documents = self.save_to_my_documents_default

        current_time = datetime.datetime.now()

        template_name_part = self.current_template if self.current_template else "新規ファイル"

        if include_time:
            date_part = current_time.strftime("%Y%m%d_%H%M")
        else:
            date_part = current_time.strftime("%Y%m%d")

        filename = f"{date_part}_{template_name_part}.txt"

        save_directory = ""
        if to_my_documents:
            save_directory = os.path.expanduser('~/Documents')
            if not os.path.exists(save_directory):
                try:
                    os.makedirs(save_directory)
                except OSError as e:
                    messagebox.showerror("エラー", f"マイドキュメントフォルダの作成に失敗しました:\n{e}")
                    return
        else:
            save_directory = os.path.dirname(os.path.abspath(__file__))

        filepath = os.path.join(save_directory, filename)

        try:
            with open(filepath, 'w', encoding='utf-8') as f:
                f.write(self.text_editor.get("1.0", tk.END))
            messagebox.showinfo("保存完了", f"ファイルを保存しました:\n{filepath}")
            self.text_editor.edit_modified(False)

            self.current_file_path = filepath
            self.current_template = None # 日付ファイルで保存したらテンプレートとの関連は切れる
            self.current_scri_file = None # 定型文ファイルとの関連も切れる
            self.load_scri_file() # 定型文リストボックスをクリア
            self.root.title(f"{os.path.basename(filepath)} - AI Script Helper Editor")
            self.save_state_to_settings()

        except Exception as e:
            messagebox.showerror("エラー", f"ファイルの保存に失敗しました:\n{e}")

    def load_state_from_settings(self):
        """Loads the last used template or file path and fixed phrase file from the settings file."""
        if self.config.has_section('Settings'):
            last_template = self.config['Settings'].get('last_template')
            last_file_path = self.config['Settings'].get('last_file_path')
            last_scri_file_rel = self.config['Settings'].get('last_scri_file')

            # 1. テンプレートまたはファイルのロード
            # テンプレートが設定されていればそれを優先
            if last_template and last_template in self.TEMPLATE_MAP:
                self.set_template(last_template)
            # そうでなければ最後に開いたファイルを試す
            elif last_file_path and os.path.exists(last_file_path):
                self.load_file_content(last_file_path)
            # どちらもなければデフォルトテンプレートをロード
            else:
                messagebox.showinfo("初回起動", "初回起動または設定ファイルが見つからないため、「文書校正」テンプレートを読み込みます。", parent=self.root)
                self.set_template("文書校正")
            
            # 2. 定型文ファイルのロード(テンプレートやファイルロードの結果に基づいて行われる)
            # set_templateやload_file_contentがcurrent_scri_fileを設定済み
            # ただし、もしlast_scri_fileがテンプレートに紐づかない単独ファイルで、
            # last_file_pathが使われた場合に、その単独ファイルをロードする必要がある。
            # 現在のロジックでは、ファイルを開いた場合はcurrent_scri_fileをNoneにしているため、
            # 明示的にlast_scri_fileをロードする処理が必要になる。
            # しかし、ユーザーはテンプレートに紐づいた定型文を使用することが想定されるため、
            # ここでの優先順位はテンプレート > 最後に開いたファイル > last_scri_fileとする。
            # current_scri_fileがまだNoneで、かつlast_scri_file_relがある場合のみロードを試みる
            if self.current_scri_file is None and last_scri_file_rel:
                full_scri_path = last_scri_file_rel
                if not os.path.isabs(last_scri_file_rel):
                    full_scri_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), last_scri_file_rel)
                
                if os.path.exists(full_scri_path):
                    self.current_scri_file = full_scri_path
                    self.load_scri_file()
                else:
                    self.current_scri_file = None
                    self.load_scri_file() # 見つからない場合はクリア

        else: # settings.ini自体にSettingsセクションがない場合
            messagebox.showinfo("初回起動", "初回起動または設定ファイルが見つからないため、「文書校正」テンプレートを読み込みます。", parent=self.root)
            self.set_template("文書校正")


    def save_state_to_settings(self):
        """Saves the current template and/or file path settings to the file."""
        if not self.config.has_section('Settings'):
            self.config.add_section('Settings')

        if self.current_template:
            self.config['Settings']['last_template'] = self.current_template
            # テンプレートが設定されている場合、last_file_pathはクリア
            if 'last_file_path' in self.config['Settings']:
                del self.config['Settings']['last_file_path']
        else:
            # テンプレートが設定されていない場合、last_templateはクリア
            if 'last_template' in self.config['Settings']:
                del self.config['Settings']['last_template']

            if self.current_file_path:
                self.config['Settings']['last_file_path'] = self.current_file_path
            else:
                if 'last_file_path' in self.config['Settings']:
                    del self.config['Settings']['last_file_path']

        # current_scri_fileは、テンプレートを使用している場合はテンプレートに紐づくファイル、
        # テンプレートを使用していない場合は独立したファイル、というロジックに沿うように保存
        if self.current_scri_file:
            script_dir = os.path.dirname(os.path.abspath(__file__))
            try:
                # スクリプトフォルダからの相対パスで保存を試みる
                relative_path = os.path.relpath(self.current_scri_file, script_dir)
                # 相対パスが ".." を含む場合(親ディレクトリにある場合など)は絶対パスで保存
                if relative_path.startswith('..'):
                     self.config['Settings']['last_scri_file'] = self.current_scri_file
                else:
                    self.config['Settings']['last_scri_file'] = relative_path
            except ValueError:
                # パスが異なるドライブにある場合など、相対パスにできない場合は絶対パスで保存
                self.config['Settings']['last_scri_file'] = self.current_scri_file
        else:
            if 'last_scri_file' in self.config['Settings']:
                del self.config['Settings']['last_scri_file']

        try:
            with open(SETTINGS_FILE, 'w', encoding='utf-8') as configfile:
                self.config.write(configfile)
        except Exception as e:
            print(f"設定ファイルの保存に失敗しました: {e}")


    def exit_editor(self):
        """Exits the editor (with confirmation dialog for unsaved changes)."""
        if self.text_editor.edit_modified():
            response = messagebox.askyesnocancel(
                "終了の確認",
                "未保存の変更があります。終了する前に保存しますか?"
            )
            if response is True:
                self.save_file()
            elif response is False:
                self.save_state_to_settings()
                self.root.destroy()
                return
            else:
                return

        self.save_state_to_settings()
        self.root.destroy()

    def find_text(self, event=None):
        """
        検索ウィンドウを開き、テキストエディタ内で文字列を検索する。
        """
        # 既にウィンドウが開いている場合はフォーカス
        if self.search_toplevel and self.search_toplevel.winfo_exists():
            self.search_toplevel.lift()
            self.search_toplevel.focus_set()
            return

        self.search_toplevel = Toplevel(self.root)
        self.search_toplevel.title("検索")
        self.search_toplevel.transient(self.root) # 親ウィンドウを設定
        self.search_toplevel.grab_set() # モーダルにする
        self.search_toplevel.resizable(False, False)

        # ウィンドウをエディタの中央に配置
        self.search_toplevel.update_idletasks()
        x = self.root.winfo_x() + (self.root.winfo_width() / 2) - (self.search_toplevel.winfo_width() / 2)
        y = self.root.winfo_y() + (self.root.winfo_height() / 2) - (self.search_toplevel.winfo_height() / 2)
        self.search_toplevel.geometry(f"+{int(x)}+{int(y)}")

        label = tk.Label(self.search_toplevel, text="検索文字列:")
        label.grid(row=0, column=0, padx=5, pady=5, sticky="w")

        self.find_entry = tk.Entry(self.search_toplevel, width=30)
        self.find_entry.grid(row=0, column=1, padx=5, pady=5)
        self.find_entry.focus_set()
        self.find_entry.bind("<Return>", self._perform_find)

        # 前回検索した文字列があればセット
        if self.last_search_term:
            self.find_entry.insert(0, self.last_search_term)
            self.find_entry.select_range(0, tk.END) # 全選択

        # 大文字・小文字を区別するチェックボックス
        case_sensitive_check = tk.Checkbutton(self.search_toplevel, text="大文字・小文字を区別する", variable=self.case_sensitive)
        case_sensitive_check.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="w")

        find_button = tk.Button(self.search_toplevel, text="次を検索", command=self._perform_find)
        find_button.grid(row=2, column=0, padx=5, pady=5)

        close_button = tk.Button(self.search_toplevel, text="閉じる", command=self._close_find_replace_window)
        close_button.grid(row=2, column=1, padx=5, pady=5)

        self.search_toplevel.bind("<Escape>", lambda e: self._close_find_replace_window())
        self.search_toplevel.protocol("WM_DELETE_WINDOW", self._close_find_replace_window)

        self.text_editor.tag_remove("search", "1.0", tk.END) # 既存のハイライトをクリア

    def _perform_find(self, event=None):
        """
        検索を実行し、結果をハイライトする。
        """
        search_term = self.find_entry.get()
        if not search_term:
            messagebox.showinfo("検索", "検索する文字列を入力してください。", parent=self.search_toplevel)
            return

        self.last_search_term = search_term # 検索文字列を記憶

        self.text_editor.tag_remove("search", "1.0", tk.END) # 前回のハイライトをクリア

        # 検索開始位置を設定
        # 選択範囲があればそこから、なければカーソル位置から
        if self.text_editor.tag_ranges(tk.SEL):
            start_index = self.text_editor.index(tk.SEL_LAST)
        else:
            start_index = self.text_editor.index(tk.INSERT)

        count_var = StringVar() # 検索結果の数を格納する変数

        # 検索オプション
        options = {}
        if not self.case_sensitive.get():
            options["nocase"] = True

        # 最初は現在のカーソル位置から検索
        idx = self.text_editor.search(search_term, start_index, stopindex=tk.END, count=count_var, **options)

        if not idx: # カーソル位置以降で見つからなかった場合、先頭から再検索
            idx = self.text_editor.search(search_term, "1.0", stopindex=tk.END, count=count_var, **options)
            if idx and self.text_editor.compare(idx, ">=", start_index):
                # 最初の検索で対象をスキップしてしまった可能性があるので、再度カーソル以降で見つかった場合は、
                # カーソル以前の検索結果は無視する(つまり、最初の検索で最後まで行ったが何も見つからなかった場合のみ、
                # 最初から検索し直す)
                idx = None # リセットして本当に見つからなかったと判断
        
        if idx:
            end_index = self.text_editor.index(f"{idx}+{len(search_term)}c") # Corrected f-string
            self.text_editor.tag_add("search", idx, end_index)
            self.text_editor.mark_set(tk.INSERT, end_index) # 次回検索のためにカーソルを移動
            self.text_editor.see(end_index) # 見つかった箇所にスクロール
            self.find_start_index = end_index # 次の検索の開始位置
        else:
            messagebox.showinfo("検索", f"'{search_term}' は見つかりませんでした。", parent=self.search_toplevel)
            self.find_start_index = "1.0" # 検索位置をリセット
            self.text_editor.tag_remove("search", "1.0", tk.END) # ハイライトをクリア

    def replace_text(self, event=None):
        """
        置き換えウィンドウを開き、テキストエディタ内で文字列を置き換える。
        """
        # 既にウィンドウが開いている場合はフォーカス
        if self.replace_toplevel and self.replace_toplevel.winfo_exists():
            self.replace_toplevel.lift()
            self.replace_toplevel.focus_set()
            return

        self.replace_toplevel = Toplevel(self.root)
        self.replace_toplevel.title("置き換え")
        self.replace_toplevel.transient(self.root) # 親ウィンドウを設定
        self.replace_toplevel.grab_set() # モーダルにする
        self.replace_toplevel.resizable(False, False)

        # ウィンドウをエディタの中央に配置
        self.replace_toplevel.update_idletasks()
        x = self.root.winfo_x() + (self.root.winfo_width() / 2) - (self.replace_toplevel.winfo_width() / 2)
        y = self.root.winfo_y() + (self.root.winfo_height() / 2) - (self.replace_toplevel.winfo_height() / 2)
        self.replace_toplevel.geometry(f"+{int(x)}+{int(y)}")

        # 検索文字列
        label_find = tk.Label(self.replace_toplevel, text="検索文字列:")
        label_find.grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.replace_find_entry = tk.Entry(self.replace_toplevel, width=30)
        self.replace_find_entry.grid(row=0, column=1, padx=5, pady=5)
        self.replace_find_entry.focus_set()
        self.replace_find_entry.bind("<Return>", self._perform_replace_find)

        # 前回検索した文字列があればセット
        if self.last_search_term:
            self.replace_find_entry.insert(0, self.last_search_term)
            self.replace_find_entry.select_range(0, tk.END)

        # 置き換え文字列
        label_replace = tk.Label(self.replace_toplevel, text="置き換え文字列:")
        label_replace.grid(row=1, column=0, padx=5, pady=5, sticky="w")
        self.replace_with_entry = tk.Entry(self.replace_toplevel, width=30)
        self.replace_with_entry.grid(row=1, column=1, padx=5, pady=5)
        self.replace_with_entry.bind("<Return>", self._perform_replace)

        # 大文字・小文字を区別するチェックボックス
        case_sensitive_check = tk.Checkbutton(self.replace_toplevel, text="大文字・小文字を区別する", variable=self.case_sensitive)
        case_sensitive_check.grid(row=2, column=0, columnspan=2, padx=5, pady=5, sticky="w")

        # ボタン
        find_button = tk.Button(self.replace_toplevel, text="次を検索", command=self._perform_replace_find)
        find_button.grid(row=3, column=0, padx=5, pady=5)

        replace_button = tk.Button(self.replace_toplevel, text="置換", command=self._perform_replace)
        replace_button.grid(row=3, column=1, padx=5, pady=5)

        replace_all_button = tk.Button(self.replace_toplevel, text="すべて置換", command=self._perform_replace_all)
        replace_all_button.grid(row=4, column=0, padx=5, pady=5)

        close_button = tk.Button(self.replace_toplevel, text="閉じる", command=self._close_find_replace_window)
        close_button.grid(row=4, column=1, padx=5, pady=5)

        self.replace_toplevel.bind("<Escape>", lambda e: self._close_find_replace_window())
        self.replace_toplevel.protocol("WM_DELETE_WINDOW", self._close_find_replace_window)

        self.text_editor.tag_remove("search", "1.0", tk.END) # 既存のハイライトをクリア

    def _perform_replace_find(self, event=None):
        """
        置き換えウィンドウから検索を実行し、結果をハイライトする。
        """
        search_term = self.replace_find_entry.get()
        if not search_term:
            messagebox.showinfo("検索", "検索する文字列を入力してください。", parent=self.replace_toplevel)
            return

        self.last_search_term = search_term # 検索文字列を記憶

        self.text_editor.tag_remove("search", "1.0", tk.END) # 前回のハイライトをクリア

        # 検索開始位置を設定
        # 選択範囲があればそこから、なければカーソル位置から
        if self.text_editor.tag_ranges(tk.SEL):
            start_index = self.text_editor.index(tk.SEL_LAST)
        else:
            start_index = self.text_editor.index(tk.INSERT)

        count_var = StringVar()

        options = {}
        if not self.case_sensitive.get():
            options["nocase"] = True

        idx = self.text_editor.search(search_term, start_index, stopindex=tk.END, count=count_var, **options)

        if not idx: # カーソル位置以降で見つからなかった場合、先頭から再検索
            idx = self.text_editor.search(search_term, "1.0", stopindex=tk.END, count=count_var, **options)
            if idx and self.text_editor.compare(idx, ">=", start_index):
                idx = None
        
        if idx:
            end_index = self.text_editor.index(f"{idx}+{len(search_term)}c") # Corrected f-string
            self.text_editor.tag_add("search", idx, end_index)
            self.text_editor.mark_set(tk.INSERT, end_index) # 次回検索のためにカーソルを移動
            self.text_editor.see(end_index) # 見つかった箇所にスクロール
            self.find_start_index = end_index # 次の検索の開始位置
        else:
            messagebox.showinfo("検索", f"'{search_term}' は見つかりませんでした。", parent=self.replace_toplevel)
            self.find_start_index = "1.0" # 検索位置をリセット
            self.text_editor.tag_remove("search", "1.0", tk.END) # ハイライトをクリア

    def _perform_replace(self):
        """
        選択された(ハイライトされた)文字列を置き換え、次の文字列を検索する。
        """
        search_term = self.replace_find_entry.get()
        replace_term = self.replace_with_entry.get()

        if not search_term:
            messagebox.showinfo("置換", "検索文字列を入力してください。", parent=self.replace_toplevel)
            return

        # 現在選択されている(ハイライトされている)検索結果を取得
        # _perform_replace_find でタグ 'search' が設定されていることを前提とする
        ranges = self.text_editor.tag_ranges("search")
        if not ranges:
            messagebox.showinfo("置換", "置き換える検索結果がありません。まず「次を検索」してください。", parent=self.replace_toplevel)
            return

        # 選択されている範囲を置き換える
        start_idx = ranges[0]
        end_idx = ranges[1]
        
        self.text_editor.delete(start_idx, end_idx)
        self.text_editor.insert(start_idx, replace_term)

        # 置換後にハイライトを解除し、次の検索を開始
        self.text_editor.tag_remove("search", "1.0", tk.END)
        self.text_editor.mark_set(tk.INSERT, f"{start_idx}+{len(replace_term)}c") # カーソルを置換後の文字列の末尾に移動
        self._perform_replace_find() # 次の検索を実行

    def _perform_replace_all(self):
        """
        すべての検索結果を置き換える。
        """
        search_term = self.replace_find_entry.get()
        replace_term = self.replace_with_entry.get()

        if not search_term:
            messagebox.showinfo("すべて置換", "検索文字列を入力してください。", parent=self.replace_toplevel)
            return

        confirm = messagebox.askyesno(
            "すべて置換の確認",
            f"エディタ内の '{search_term}' をすべて '{replace_term}' に置き換えますか?\nこの操作は元に戻せません。"
        , parent=self.replace_toplevel)

        if not confirm:
            return

        self.text_editor.tag_remove("search", "1.0", tk.END) # 既存のハイライトをクリア
        
        count = 0
        start_pos = "1.0"

        options = {}
        if not self.case_sensitive.get():
            options["nocase"] = True

        while True:
            idx = self.text_editor.search(search_term, start_pos, stopindex=tk.END, **options)
            if not idx:
                break
            
            end_idx = self.text_editor.index(f"{idx}+{len(search_term)}c")
            self.text_editor.delete(idx, end_idx)
            self.text_editor.insert(idx, replace_term)
            
            start_pos = self.text_editor.index(f"{idx}+{len(replace_term)}c") # 次の検索は置換後の文字列の末尾から
            count += 1
        
        messagebox.showinfo("すべて置換", f"{count} 件の置き換えが完了しました。", parent=self.replace_toplevel)
        self.text_editor.edit_modified(True) # 変更があったことをマーク
        self._update_line_numbers() # 行番号も更新

    def _close_find_replace_window(self):
        """検索/置き換えウィンドウを閉じる。"""
        self.text_editor.tag_remove("search", "1.0", tk.END) # ハイライトをクリア
        self.find_start_index = "1.0" # 検索位置をリセット

        if self.search_toplevel and self.search_toplevel.winfo_exists():
            self.search_toplevel.destroy()
            self.search_toplevel = None
        if self.replace_toplevel and self.replace_toplevel.winfo_exists():
            self.replace_toplevel.destroy()
            self.replace_toplevel = None
        self.root.focus_set() # メインウィンドウにフォーカスを戻す


def check_and_install_libraries():
    """Checks if required libraries are installed and prompts for installation."""
    required_libraries = []
    for lib in required_libraries:
        try:
            __import__(lib)
        except ImportError:
            response = messagebox.askyesno(
                "ライブラリのインストール",
                f"'{lib}' ライブラリが見つかりません。\nインストールしますか?",
                parent=root # Ensure parent is set for messagebox
            )
            if response:
                try:
                    subprocess.check_call([sys.executable, "-m", "pip", "install", lib])
                    messagebox.showinfo("インストール完了", f"'{lib}' が正常にインストールされました。", parent=root)
                except Exception as e:
                    messagebox.showerror("インストールエラー", f"'{lib}' のインストールに失敗しました:\n{e}", parent=root)
                    sys.exit(1)
            else:
                messagebox.showwarning("警告", f"'{lib}' がインストールされていないため、一部機能が制限される可能性があります。", parent=root)

def main():
    """Main function of the application."""
    global root # Declare root as global so check_and_install_libraries can access it
    root = tk.Tk()
    app = ScriptHelperEditor(root)
    root.mainloop()

if __name__ == "__main__":
    main()