Search code examples
pythonpython-3.xtkintertkinter-text

How to get the line index of the current line


I'm making a Test Editor from a base that I got from a YouTube tutorial. I was trying to make highlighted the python's statements, but when I write a statement, it colorizes all the lines, and I thought that the problem is the use of indexes I make.

This is the code:

import tkinter as tk
from tkinter import filedialog
from tkinter import messagebox


class Menubar:

    def __init__(self, parent):
        font_specs = 14
        
        menubar = tk.Menu(parent.master)
        parent.master.config(menu = menubar)

        file_dropdown = tk.Menu(menubar, font = font_specs, tearoff = 0)
        file_dropdown.add_command(label = "Nuovo file",
                                  accelerator = "Ctrl + N",
                                  command = parent.new_file)

        file_dropdown.add_command(label = "Apri file",
                                  accelerator = "Ctrl + O",
                                  command = parent.open_file)

        file_dropdown.add_command(label = "Salva",
                                  accelerator = "Ctrl + S",
                                  command = parent.save)

        file_dropdown.add_command(label = "Salva con nome",
                                  accelerator = "Ctrl + Shit + S",
                                  command = parent.save_as)

        file_dropdown.add_separator()

        file_dropdown.add_command(label = "Esci",
                                  command = parent.master.destroy)

        about_dropdown = tk.Menu(menubar, font = font_specs, tearoff = 0)
        about_dropdown.add_command(label = "Note di rilascio",
                                   command = self.show_about_message)

        about_dropdown.add_separator()

        about_dropdown.add_command(label = "About",
                                   command = self.show_release_notes)

        settings_dropdown = tk.Menu(menubar, font = font_specs, tearoff = 0)
        settings_dropdown.add_command(label = "Cambia lo sfondo dell'editor",
                                      command = parent.change_background)

        menubar.add_cascade(label = "File", menu = file_dropdown)
        menubar.add_cascade(label = "About", menu = about_dropdown)
        menubar.add_cascade(label = "Settings", menu = settings_dropdown)


    def show_about_message(self):
        box_title = "Riguardo PyText"
        box_message = "Il mio primo editor testuale creato con Python e TkInter!"

        messagebox.showinfo(box_title, box_message)


    def show_release_notes(self):
        box_title = "Note di Rilascio"
        box_message = "Versione 0.1 (Beta) Santa"

        messagebox.showinfo(box_title, box_message)
        

class Statusbar:

    def __init__(self, parent):
        font_specs = 12

        self.status = tk.StringVar()
        self.status.set("PyText - 0.1 Santa")

        label = tk.Label(parent.text_area, textvariable = self.status,
                         fg = "black", bg = "lightgrey", anchor = "sw")

        label.pack(side = tk.BOTTOM, fill = tk.BOTH)

    def update_status(self, *args):
        if isinstance(args[0], bool):
            self.status.set("Il tuo file è stato salvato!")

        else:
            self.status.set("PyText - 0.1 Santa")


class PyText:
    """
    Classe-Madre dell'applicazione
    """

    def __init__(self, master):
        master.title("Untitled - PyText")
        master.geometry("1200x700")

        font_specs = 18
        
        self.master = master
        self.filename = None
        
        self.text_area = tk.Text(master, font = font_specs, insertbackground = "black")
        self.scroll = tk.Scrollbar(master, command = self.text_area.yview)
        self.text_area.configure(yscrollcommand = self.scroll.set)
        self.text_area.pack(side = tk.LEFT, fill = tk.BOTH, expand = True)
        self.scroll.pack(side = tk.RIGHT, fill = tk.Y)

        self.menubar = Menubar(self)
        self.statusbar = Statusbar(self)
        self.bind_shortcuts()

        
    def set_window_title(self, name = None):
        if name:
            self.master.title(name + " - PyText")
            
        else:
            self.master.title("Untitled - PyText")    


    def new_file(self, *args):
        self.text_area.delete(1.0, tk.END)
        self.filename = None

        self.set_window_title()


    def open_file(self, *args):
        self.filename = filedialog.askopenfilename(
            defaultextension = ".txt",
            filetypes = [("Tutti i file", "*.*"),
                         ("File di Testo", "*.txt"),
                         ("Script Python", "*.py"),
                         ("Markdown Text", "*.md"),
                         ("File JavaScript", "*.js"),
                         ("Documenti Html", "*.html"),
                         ("Documenti CSS", "*.css"),
                         ("Programmi Java", "*.java")]
        )

        if self.filename:
            self.text_area.delete(1.0, tk.END)

            with open(self.filename, "r") as f:
                self.text_area.insert(1.0, f.read())

            self.set_window_title(self.filename)


    def save(self, *args):
        if self.filename:
            try:
                textarea_content = self.text_area.get(1.0, tk.END)
                with open(self.filename, "w") as f:
                    f.write(textarea_content)

                self.statusbar.update_status(True)

            except Exception as e:
                print(e)

        else:
            self.save_as()


    def save_as(self, *args):
        try:
            new_file = filedialog.asksaveasfilename(
                initialfile = "Untitled.txt",
                defaultextension = ".txt",
                filetypes = [("Tutti i file", "*.*"),
                             ("File di Testo", "*.txt"),
                             ("Script Python", "*.py"),
                             ("Markdown Text", "*.md"),
                             ("File JavaScript", "*.js"),
                             ("Documenti Html", "*.html"),
                             ("Documenti CSS", "*.css"),
                             ("Programmi Java", "*.java")]
                )

            textarea_content = self.text_area.get(1.0, tk.END)
            with open(new_file, "w") as f:
                f.write(textarea_content)

            self.filename = new_file
            self.set_window_title(self.filename)
            self.statusbar.update_status(True)

        except Exception as e:
            print(e)


    def change_background(self):
        self.text_area.config(background = "black", foreground = "white", 
                              insertbackground = "white", insertwidth = 2)

        
    def highlight_text(self, *args):
        tags = {
            "import": "pink",
            "from": "pink",
            "def": "blue",
            "for": "purple",
            "while": "purple"
        }

        textarea_content = self.text_area.get(1.0, tk.END)
        lines = textarea_content.split("\n")

        for row in lines:
            for tag in tags:
                index = row.find(tag) + 1.0

                if index > 0.0:
                    self.text_area.tag_add(tag, index, index + len(tag) -1)
                    self.text_area.tag_config(tag, foreground = tags.get(tag))

                    print("Nuovo tag aggiunto:", tag)
            
        print("Funzione eseguita:", args, "\n")
    
    
    def bind_shortcuts(self):
        self.text_area.bind("<Control-n>", self.new_file)
        self.text_area.bind("<Control-o>", self.open_file)
        self.text_area.bind("<Control-s>", self.save)
        self.text_area.bind("<Control-S>", self.save_as)
        self.text_area.bind("<Key>", self.highlight_text, self.statusbar.update_status)

    
if __name__ == "__main__":
    master = tk.Tk()
    pt = PyText(master)
    master.mainloop()

How can I get the index of the line where is the statement?


Solution

  • You works with every line separtelly so you get only X. To get Y you need to enumerate lines:

    for y, row in enumerate(lines, 1):
    

    Result from find() you convert to float but you need int and later convert X,Y to string "Y.X"

    start = '{}.{}'.format(y, x)
    end   = '{}.{}'.format(y, x+len(tag))
    

    Version which works for me

    for y, row in enumerate(lines, 1):
        for tag in tags:
    
            x = row.find(tag)
    
            if x > -1:
                print(f"{tag} | x: {x} | y: {y}")
    
                start = '{}.{}'.format(y, x)
                end   = '{}.{}'.format(y, x+len(tag))
    
                print(f"{tag} | start: {start} | end: {end}")
    
                self.text_area.tag_add(tag, start, end)
                self.text_area.tag_config(tag, foreground = tags.get(tag))
    

    Your idea has only one big problem - it will colorize def in word define, or for in word forward, etc. So maybe it would need regex to create more complex rules and search only full words.

    Other problem: find() gives you only first item in line - so if you will try to highlight element which can be two times in line then you would have to use loop with find(..., start) to search after first element.

    Maybe it would be better to use example for How to highlight text in a tkinter Text widget


    BTW:

    You binded key shortcuts to self.text_area so I had to click in text area to use shortcut. If I change self.text_area.bind into self.master.bind then shortcuts works even without clicking in text area.


    EDIT:

    There is Thonny IDE which uses Tkinter and it highlights code.

    I tried to find how it makes this but I found only regex for highlights - thonny_utils.py

    Maybe if you get full code and use some tool to search string in files (like grep in Linux) then you could find all places where it uses variables from token_utils.py (ie. KEYWORD)

    EDIT: coloring.py


    EDIT:

    Full code with function highlight_text which uses previous method line.find and highlight_text_regex which uses text_area.search with regex.

    New version based on code from answer to question How to highlight text in a tkinter Text widget

    enter image description here

    import tkinter as tk
    from tkinter import filedialog
    from tkinter import messagebox
    import os
    
    print(os.getcwd())
    
    class Menubar:
    
        def __init__(self, parent):
            font_specs = 14
            
            menubar = tk.Menu(parent.master)
            parent.master.config(menu = menubar)
    
            file_dropdown = tk.Menu(menubar, font = font_specs, tearoff = 0)
            file_dropdown.add_command(label = "Nuovo file",
                                      accelerator = "Ctrl + N",
                                      command = parent.new_file)
    
            file_dropdown.add_command(label = "Apri file",
                                      accelerator = "Ctrl + O",
                                      command = parent.open_file)
    
            file_dropdown.add_command(label = "Salva",
                                      accelerator = "Ctrl + S",
                                      command = parent.save)
    
            file_dropdown.add_command(label = "Salva con nome",
                                      accelerator = "Ctrl + Shift + S",
                                      command = parent.save_as)
    
            file_dropdown.add_separator()
    
            file_dropdown.add_command(label = "Esci",
                                      accelerator = "Ctrl + Q",
                                      command = parent.master.destroy)
    
            about_dropdown = tk.Menu(menubar, font = font_specs, tearoff = 0)
            about_dropdown.add_command(label = "Note di rilascio",
                                       command = self.show_about_message)
    
            about_dropdown.add_separator()
    
            about_dropdown.add_command(label = "About",
                                       command = self.show_release_notes)
    
            settings_dropdown = tk.Menu(menubar, font = font_specs, tearoff = 0)
            settings_dropdown.add_command(label = "Cambia lo sfondo dell'editor",
                                          command = parent.change_background)
    
            menubar.add_cascade(label = "File", menu = file_dropdown)
            menubar.add_cascade(label = "About", menu = about_dropdown)
            menubar.add_cascade(label = "Settings", menu = settings_dropdown)
    
    
        def show_about_message(self):
            box_title = "Riguardo PyText"
            box_message = "Il mio primo editor testuale creato con Python e TkInter!"
    
            messagebox.showinfo(box_title, box_message)
    
    
        def show_release_notes(self):
            box_title = "Note di Rilascio"
            box_message = "Versione 0.1 (Beta) Santa"
    
            messagebox.showinfo(box_title, box_message)
            
    
    class Statusbar:
    
        def __init__(self, parent):
            font_specs = 12
    
            self.status = tk.StringVar()
            self.status.set("PyText - 0.1 Santa")
    
            label = tk.Label(parent.text_area, textvariable = self.status,
                             fg = "black", bg = "lightgrey", anchor = "sw")
    
            label.pack(side = tk.BOTTOM, fill = tk.BOTH)
    
        def update_status(self, *args):
            if isinstance(args[0], bool):
                self.status.set("Il tuo file è stato salvato!")
    
            else:
                self.status.set("PyText - 0.1 Santa")
    
    
    class PyText:
        """
        Classe-Madre dell'applicazione
        """
    
        def __init__(self, master):
            master.title("Untitled - PyText")
            master.geometry("1200x700")
    
            font_specs = 18
            
            self.master = master
            self.filename = None
            
            self.text_area = tk.Text(master, font = font_specs, insertbackground = "black")
            self.scroll = tk.Scrollbar(master, command = self.text_area.yview)
            self.text_area.configure(yscrollcommand = self.scroll.set)
            self.text_area.pack(side = tk.LEFT, fill = tk.BOTH, expand = True)
            self.scroll.pack(side = tk.RIGHT, fill = tk.Y)
    
            self.menubar = Menubar(self)
            self.statusbar = Statusbar(self)
            self.bind_shortcuts()
    
            
        def set_window_title(self, name = None):
            if name:
                self.master.title(name + " - PyText")
                
            else:
                self.master.title("Untitled - PyText")    
    
    
        def new_file(self, *args):
            self.text_area.delete(1.0, tk.END)
            self.filename = None
    
            self.set_window_title()
    
    
        def open_file(self, *args):
            self.filename = filedialog.askopenfilename(
                initialdir = os.getcwd(),
                defaultextension = ".txt",
                filetypes = [("Tutti i file", "*.*"),
                             ("File di Testo", "*.txt"),
                             ("Script Python", "*.py"),
                             ("Markdown Text", "*.md"),
                             ("File JavaScript", "*.js"),
                             ("Documenti Html", "*.html"),
                             ("Documenti CSS", "*.css"),
                             ("Programmi Java", "*.java")]
            )
    
            if self.filename:
                self.text_area.delete(1.0, tk.END)
    
                with open(self.filename, "r") as f:
                    self.text_area.insert(1.0, f.read())
    
                self.set_window_title(self.filename)
    
    
        def save(self, *args):
            if self.filename:
                try:
                    textarea_content = self.text_area.get(1.0, tk.END)
                    with open(self.filename, "w") as f:
                        f.write(textarea_content)
    
                    self.statusbar.update_status(True)
    
                except Exception as e:
                    print(e)
    
            else:
                self.save_as()
    
    
        def save_as(self, *args):
            try:
                new_file = filedialog.asksaveasfilename(
                    initialfile = "Untitled.txt",
                    defaultextension = ".txt",
                    filetypes = [("Tutti i file", "*.*"),
                                 ("File di Testo", "*.txt"),
                                 ("Script Python", "*.py"),
                                 ("Markdown Text", "*.md"),
                                 ("File JavaScript", "*.js"),
                                 ("Documenti Html", "*.html"),
                                 ("Documenti CSS", "*.css"),
                                 ("Programmi Java", "*.java")]
                    )
    
                textarea_content = self.text_area.get(1.0, tk.END)
                with open(new_file, "w") as f:
                    f.write(textarea_content)
    
                self.filename = new_file
                self.set_window_title(self.filename)
                self.statusbar.update_status(True)
    
            except Exception as e:
                print(e)
    
    
        def change_background(self):
            self.text_area.config(background = "black", foreground = "white", 
                                  insertbackground = "white", insertwidth = 2)
    
            
        def highlight_text_old(self, *args):
            tags = {
                "import": "pink",
                "from": "red",
                "def": "blue",
                "for": "purple",
                "while": "green",
            }
    
            textarea_content = self.text_area.get(1.0, tk.END)
            lines = textarea_content.split("\n")
    
            for y, row in enumerate(lines, 1):
                for tag in tags:
                    x = row.find(tag)
                    if x > -1:
                        print(f"{tag} | x: {x} | y: {y}")
                        start = '{}.{}'.format(y, x)
                        end   = '{}.{}'.format(y, x+len(tag))
                        print(f"{tag} | start: {start} | end: {end}")
                        self.text_area.tag_add(tag, start, end)
                        self.text_area.tag_config(tag, foreground = tags.get(tag))
    
                        #print("Nuovo tag aggiunto:", tag)
                
            #print("Funzione eseguita:", args, "\n")
    
        def highlight_text(self, *args):
            # TODO: move to `__init__` ?
            tags = {
                "import": "pink",
                "from": "red",
                "def": "blue",
                "for": "purple",
                "while": "green",
            }
    
            # TODO: move to `__init__` ?
            # create tags with assigned color - do it only onve (in __init__)
            for color in ['pink', 'red', 'blue', 'purple', 'green']:
                self.text_area.tag_config(color, foreground=color)
    
            # remove all tags from text
            for tag in self.text_area.tag_names():
                self.text_area.tag_remove(tag, '1.0', 'end')  # not `tag_remove()`
             
    
            textarea_content = self.text_area.get(1.0, tk.END)
            lines = textarea_content.split("\n")
    
            for y, row in enumerate(lines, 1):
                for tag in tags:
                    x = row.find(tag)
                    if x > -1:
                        print(f"{tag} | x: {x} | y: {y}")
                        match_start = '{}.{}'.format(y, x)
                        match_end   = '{}.{}'.format(y, x+len(tag))
                        print(f"{tag} | start: {match_start} | end: {match_end}")
                        self.text_area.tag_add(tag, match_start, match_end)
                        #self.text_area.tag_config(tag, foreground=tags.get(tag))  # create tags only once - at start
    
                        #print("Nuovo tag aggiunto:", tag)
                
            #print("Funzione eseguita:", args, "\n")
    
    
        def highlight_text_regex(self, *args):
            # TODO: move to `__init__` ?
            tags = {
                "import": "red",
                "from": "red",
                "as": "red",
    
                "def": "blue",
                "class": "blue",
    
                "for": "green",
                "while": "green",
    
                "if": "brown",
                "elif": "brown",
                "else": "brown",
    
                "print": "purple",            
    
                "True": "blue",
                "False": "blue",
                "self": "blue",
    
                "\d+": "red",  # digits
    
                "__[a-zA-Z][a-zA-Z0-9_]*__": "red",  # ie. `__init__`
            }
    
            # add `\m \M` to words
            tags = {f'\m{word}\M': tag for word, tag in tags.items()}
    
            # tags which doesn't work with  `\m \M`
            other_tags = {
                "\(": "brown",  # need `\` because `(` has special meaning
                "\)": "brown",  # need `\` because `)` has special meaning
    
                ">=": "green",
                "<=": "green",
                "=": "green",
                ">": "green",
                "<": "green",
    
                "#.*$": "brown",  # comment - to the end of line `$`
            }
    
            # create one dictionary with all tags
            tags.update(other_tags)
    
            # TODO: move to `__init__` ?
            # create tags with assigned color - do it only onve (in __init__)
            for color in ['pink', 'red', 'blue', 'purple', 'green', 'brown', 'yellow']:
                self.text_area.tag_config(color, foreground=color)
    
            # remove all tags from text before adding all tags again (to change color when ie. `def` change to `define`)
            for tag in self.text_area.tag_names():
                self.text_area.tag_remove(tag, '1.0', 'end')  # not `tag_remove()`
    
            count_chars = tk.IntVar() # needs to count matched chars - ie. for digits `\d+`
            # search `word` and add `tag`
            for word, tag in tags.items():
                #pattern = f'\m{word}\M'  # http://tcl.tk/man/tcl8.5/TclCmd/re_syntax.htm#M72
                pattern = word  # http://tcl.tk/man/tcl8.5/TclCmd/re_syntax.htm#M72
                search_start = '1.0'
                search_end   = 'end'
                while True:
                    position = self.text_area.search(pattern, search_start, search_end, count=count_chars, regexp=True)
                    print('search:', word, position)
                    if position:
                        print(f"{word} | pos: {position}")
                        match_start = position
                        match_end   = '{}+{}c'.format(position, count_chars.get()) #len(word)) # use special string `Y.X+Nc` instead values (Y, X+N)
                        print(f"{word} | start: {match_start} | end: {match_end}")
                        self.text_area.tag_add(tag, match_start, match_end)
                        #self.text_area.tag_config(tag, foreground=tags.get(tag))  # create tags only once - at start
                        search_start = match_end  # to search next word
                    else:
                        break    
    
        def quit(self, *args):
            self.master.destroy()
    
        def bind_shortcuts(self):
            self.master.bind("<Control-n>", self.new_file)
            self.master.bind("<Control-o>", self.open_file)
            self.master.bind("<Control-s>", self.save)
            self.master.bind("<Control-S>", self.save_as)
            self.master.bind("<Control-q>", self.quit)
            self.master.bind("<Key>", self.highlight_text_regex, self.statusbar.update_status)
    
        
    if __name__ == "__main__":
        master = tk.Tk()
        pt = PyText(master)
        master.mainloop()