テキストエディタ『漆黒編纂典蔵』 ソースコード

以下は『漆黒編纂典蔵』のソースコード全文です:



        import tkinter as tk
        from tkinter import filedialog, messagebox
        from tkinter import ttk
        from pygments import lex
        from pygments.lexers import get_lexer_by_name
        from pygments.styles import get_style_by_name
        import os
        import re
        
        class TextEditor:
            def __init__(self, root):
                self.root = root
                self.root.title("テキストエディタ - 漆黒編纂典蔵")
                self.root.geometry("800x600")
        
                self.filename = None
                self.language_mode = "python"
                self.theme_mode = "dark"
                self.wrap_mode = False
                self.highlight_zenkaku = False
        
                self.comment_tokens = {
                    "python": "#",
                    "javascript": "//",
                    "html": "<!-- -->",
                    "c": "//",
                    "cpp": "//",
                    "text": "#"
                }
                self.current_comment_token = self.comment_tokens[self.language_mode]
        
                self._setup_widgets()
                self._set_lexer("python")
                self.set_dark_theme()
                self._configure_tags()
                self._update_language_display()
        
            def _setup_widgets(self):
                menubar = tk.Menu(self.root)
        
                file_menu = tk.Menu(menubar, tearoff=0)
                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_file_as)
                menubar.add_cascade(label="ファイル", menu=file_menu)
        
                edit_menu = tk.Menu(menubar, tearoff=0)
                edit_menu.add_command(label="コピー", command=self.copy_all_text)
                edit_menu.add_command(label="クリア", command=self.clear_text)
                edit_menu.add_command(label="コメント", command=self.comment_selection)
                edit_menu.add_command(label="アンコメント", command=self.uncomment_selection)
                edit_menu.add_command(label="行頭に文字追加", command=self.add_prefix_to_lines)
                edit_menu.add_command(label="行頭から文字削除", command=self.remove_prefix_by_length)
                menubar.add_cascade(label="編集", menu=edit_menu)
        
                view_menu = tk.Menu(menubar, tearoff=0)
                view_menu.add_command(label="ダークモード", command=self.set_dark_theme)
                view_menu.add_command(label="ライトモード", command=self.set_light_theme)
                view_menu.add_command(label="折り返し ON", command=lambda: self.toggle_wrap(True))
                view_menu.add_command(label="折り返し OFF", command=lambda: self.toggle_wrap(False))
                self.zenkaku_var = tk.BooleanVar(value=False)
                view_menu.add_checkbutton(label="全角ハイライト", command=self.toggle_zenkaku_highlight, variable=self.zenkaku_var)
                menubar.add_cascade(label="表示", menu=view_menu)
        
                lang_menu = tk.Menu(menubar, tearoff=0)
                for lang in ["python", "javascript", "html", "c", "cpp"]:
                    lang_menu.add_command(label=lang, command=lambda l=lang: self.change_language(l))
                menubar.add_cascade(label="言語モード", menu=lang_menu)
        
                search_menu = tk.Menu(menubar, tearoff=0)
                search_menu.add_command(label="検索", command=self.open_search_window)
                menubar.add_cascade(label="検索", menu=search_menu)
        
                self.root.config(menu=menubar)
        
                label_frame = tk.Frame(self.root)
                label_frame.pack(fill=tk.X)
                self.language_label = ttk.Label(label_frame, text="", anchor="e")
                self.language_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
                self.wrap_label = ttk.Label(label_frame, text="折り返し: OFF", anchor="e")
                self.wrap_label.pack(side=tk.RIGHT)
        
                text_frame = tk.Frame(self.root)
                text_frame.pack(fill=tk.BOTH, expand=True)
        
                self.line_numbers = tk.Text(text_frame, width=4, padx=4, takefocus=0, border=0,
                                            background='#2b2b2b', foreground='white', state='disabled')
                self.line_numbers.pack(side=tk.LEFT, fill=tk.Y)
        
                self.text = tk.Text(text_frame, undo=True, wrap="none")
                self.text.pack(fill=tk.BOTH, expand=1)
        
                scrollbar = tk.Scrollbar(self.text, command=self.on_scrollbar)
                scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
                self.text.config(yscrollcommand=scrollbar.set)
                self.text.bind("<KeyRelease>", self.on_key_release)
                self.text.bind("<MouseWheel>", self.on_mouse_wheel)
        
                self.find_str = ""
        
            def _set_lexer(self, lang):
                try:
                    self.lexer = get_lexer_by_name(lang)
                except:
                    self.lexer = get_lexer_by_name("text")
        
            def _configure_tags(self):
                style_name = "monokai" if self.theme_mode == "dark" else "default"
                style = get_style_by_name(style_name)
                for token, opts in style:
                    foreground = opts['color'] if 'color' in opts else None
                    if foreground:
                        self.text.tag_configure(str(token), foreground=f"#{foreground}")
        
            def on_key_release(self, event=None):
                self.update_line_numbers()
                self.highlight_syntax()
        
            def on_scrollbar(self, *args):
                self.text.yview(*args)
                self.line_numbers.yview(*args)
        
            def on_mouse_wheel(self, event):
                self.text.yview_scroll(int(-1*(event.delta/120)), "units")
                self.line_numbers.yview_scroll(int(-1*(event.delta/120)), "units")
                return "break"
        
            def update_line_numbers(self):
                self.line_numbers.config(state='normal')
                self.line_numbers.delete('1.0', tk.END)
                line_count = int(self.text.index('end-1c').split('.')[0]) + 1
                lines = "\n".join(str(i) for i in range(1, line_count + 1))
                self.line_numbers.insert('1.0', lines)
                self.line_numbers.config(state='disabled')
        
            def highlight_syntax(self):
                code = self.text.get("1.0", tk.END)
                self.text.mark_set("range_start", "1.0")
                for tag in self.text.tag_names():
                    self.text.tag_remove(tag, "1.0", tk.END)
                for token, content in lex(code, self.lexer):
                    self.text.mark_set("range_end", f"range_start + {len(content)}c")
                    self.text.tag_add(str(token), "range_start", "range_end")
                    self.text.mark_set("range_start", "range_end")
        
                if self.highlight_zenkaku:
                    self.text.tag_remove("zenkaku", "1.0", tk.END)
                    pattern = r"[\u3000-\u9FFF]"
                    start = "1.0"
                    while True:
                        match = self.text.search(pattern, start, regexp=True, stopindex=tk.END)
                        if not match:
                            break
                        end = f"{match}+1c"
                        self.text.tag_add("zenkaku", match, end)
                        start = end
                    self.text.tag_configure("zenkaku", background="red", foreground="white")
                else:
                    self.text.tag_remove("zenkaku", "1.0", tk.END)
        
            def set_dark_theme(self):
                self.theme_mode = "dark"
                self.text.configure(bg="#1e1e1e", fg="white", insertbackground="white")
                self.line_numbers.configure(bg="#2b2b2b", fg="white")
                self._configure_tags()
                self.highlight_syntax()
        
            def set_light_theme(self):
                self.theme_mode = "light"
                self.text.configure(bg="white", fg="black", insertbackground="black")
                self.line_numbers.configure(bg="#e0e0e0", fg="black")
                self._configure_tags()
                self.highlight_syntax()
        
            def toggle_wrap(self, state):
                self.wrap_mode = state
                self.text.config(wrap="word" if state else "none")
                self.wrap_label.config(text="折り返し: ON" if state else "折り返し: OFF")
        
            def toggle_zenkaku_highlight(self):
                self.highlight_zenkaku = not self.highlight_zenkaku
                self.highlight_syntax()
        
            def open_file(self):
                file_path = filedialog.askopenfilename(filetypes=[("All files", "*.*")])
                if file_path:
                    with open(file_path, "r", encoding="utf-8") as file:
                        self.text.delete("1.0", tk.END)
                        self.text.insert("1.0", file.read())
                        self.filename = file_path
                        self._update_title()
                    self.highlight_syntax()
        
            def save_file(self):
                if not self.filename:
                    self.save_file_as()
                    return
                with open(self.filename, "w", encoding="utf-8") as file:
                    file.write(self.text.get("1.0", tk.END))
                self._update_title()
        
            def save_file_as(self):
                file_path = filedialog.asksaveasfilename(filetypes=[("All files", "*.*")])
                if not file_path:
                    return
                self.filename = file_path
                self.save_file()
        
            def _update_title(self):
                name = os.path.basename(self.filename) if self.filename else "無題"
                self.root.title(f"Text Editor - {name}")
        
            def copy_all_text(self):
                self.root.clipboard_clear()
                self.root.clipboard_append(self.text.get("1.0", tk.END))
        
            def clear_text(self):
                self.text.delete("1.0", tk.END)
        
            def change_language(self, lang):
                self.language_mode = lang
                self._set_lexer(lang)
                self.current_comment_token = self.comment_tokens.get(lang, "#")
                self._configure_tags()
                self._update_language_display()
                self.highlight_syntax()
        
            def _update_language_display(self):
                self.language_label.config(text=f"現在の言語モード:{self.language_mode}")
        
            def comment_selection(self):
                try:
                    start = self.text.index("sel.first linestart")
                    end = self.text.index("sel.last lineend")
                except tk.TclError:
                    messagebox.showinfo("情報", "コメントする範囲を選択してください。")
                    return
                lines = self.text.get(start, end).split("\n")
        
                if self.language_mode == "html":
                    self.text.delete(start, end)
                    self.text.insert(start, f"<!--\n{chr(10).join(lines)}\n-->")
                else:
                    commented = [self.current_comment_token + " " + line for line in lines]
                    self.text.delete(start, end)
                    self.text.insert(start, "\n".join(commented))
        
            def uncomment_selection(self):
                try:
                    start = self.text.index("sel.first linestart")
                    end = self.text.index("sel.last lineend")
                except tk.TclError:
                    messagebox.showinfo("情報", "アンコメントする範囲を選択してください。")
                    return
                lines = self.text.get(start, end).split("\n")
        
                if self.language_mode == "html":
                    text = "\n".join(lines)
                    if text.startswith("<!--") and text.endswith("-->"):
                        text = text[4:-3].strip("\n")
                        self.text.delete(start, end)
                        self.text.insert(start, text)
                else:
                    prefix = self.current_comment_token + " "
                    uncommented = [line[len(prefix):] if line.startswith(prefix) else line for line in lines]
                    self.text.delete(start, end)
                    self.text.insert(start, "\n".join(uncommented))
        
            def add_prefix_to_lines(self):
                prefix = self.prompt_user("行頭に追加する文字列を入力してください:")
                if not prefix:
                    return
                try:
                    start = self.text.index("sel.first linestart")
                    end = self.text.index("sel.last lineend")
                except tk.TclError:
                    messagebox.showinfo("情報", "範囲を選択してください。")
                    return
                lines = self.text.get(start, end).split("\n")
                new_lines = [prefix + line for line in lines]
                self.text.delete(start, end)
                self.text.insert(start, "\n".join(new_lines))
        
            def remove_prefix_by_length(self):
                length_str = self.prompt_user("行頭から削除する文字数を入力してください:")
                if not length_str or not length_str.isdigit():
                    return
                length = int(length_str)
                try:
                    start = self.text.index("sel.first linestart")
                    end = self.text.index("sel.last lineend")
                except tk.TclError:
                    messagebox.showinfo("情報", "範囲を選択してください。")
                    return
                lines = self.text.get(start, end).split("\n")
                new_lines = [line[length:] if len(line) >= length else '' for line in lines]
                self.text.delete(start, end)
                self.text.insert(start, "\n".join(new_lines))
        
            def prompt_user(self, message):
                popup = tk.Toplevel(self.root)
                popup.title("入力")
        
                label = tk.Label(popup, text=message)
                label.pack(padx=10, pady=5)
        
                entry = tk.Entry(popup)
                entry.pack(padx=10, pady=5)
                entry.focus_set()
        
                result = {}
        
                def on_submit(event=None):
                    result["text"] = entry.get()
                    popup.destroy()
        
                entry.bind("<Return>", on_submit)
                button = tk.Button(popup, text="OK", command=on_submit)
                button.pack(pady=5)
        
                self.root.wait_window(popup)
                return result.get("text", "")
        
            def open_search_window(self):
                search_window = tk.Toplevel(self.root)
                search_window.title("検索")
        
                def on_close():
                    self.text.tag_remove("highlight", "1.0", tk.END)
                    search_window.destroy()
        
                search_window.protocol("WM_DELETE_WINDOW", on_close)
        
                search_label = ttk.Label(search_window, text="検索する文字列:")
                search_label.pack(side=tk.LEFT, padx=10)
        
                search_entry = ttk.Entry(search_window)
                search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
                search_entry.focus_set()
        
                def on_search_button():
                    self.on_search(search_entry.get())
        
                search_entry.bind("<Return>", lambda event: on_search_button())
                search_button = tk.Button(search_window, text="検索", command=on_search_button)
                search_button.pack(side=tk.RIGHT)
        
            def on_search(self, search_str):
                self.find_str = search_str
                self.highlight_search_results()
        
            def highlight_search_results(self):
                self.text.tag_remove("highlight", "1.0", tk.END)
                pattern = self.find_str
                start_pos = "1.0"
                while True:
                    start_pos = self.text.search(pattern, start_pos, nocase=True, stopindex=tk.END)
                    if not start_pos:
                        break
                    end_pos = f"{start_pos}+{len(self.find_str)}c"
                    self.text.tag_add("highlight", start_pos, end_pos)
                    start_pos = end_pos
                self.text.tag_configure("highlight", background="yellow")
        
        if __name__ == "__main__":
            root = tk.Tk()
            app = TextEditor(root)
            root.mainloop()