import pygame as pg from pygame import Rect from piece_table import PieceTable import os import json import sys import time import copy from dataclasses import dataclass from typing import List, Optional from code import InteractiveConsole, InteractiveInterpreter @dataclass(slots=True) class Cursor: width: float height: float line_num: int = 0 col: int = 0 want_col: int = 0 @dataclass(slots=True) class Line: # Making this a PieceTable rather than a string is rather pointless right now # But it's easier to operate on rows for the time being piece: PieceTable length: int @dataclass(slots=True) class Buffer: lines: List[Line] cursor: Cursor filepath: Optional[str] = None def cursor_pos(cursor: Cursor): return (cursor.line_num, cursor.col) def cursor_draw(cursor: Cursor): rect = Rect(cursor.col * cursor.width, cursor.line_num * cursor.height, 3, cursor.height) pg.draw.rect(screen, "black", rect, width=2) def line_create(text: str) -> Line: return Line(PieceTable(text), len(text)) def buffer_create(text: str, filepath: str, cursor: Cursor) -> Buffer: lines = [] for line in text.split('\n'): lines.append(line_create(line)) return Buffer(lines, cursor, filepath) def buffer_line_current(buf: Buffer) -> Line: return buf.lines[buf.cursor.line_num] def buffer_line_num(buf: Buffer) -> int: return buf.cursor.line_num def buffer_total_lines(buf: Buffer) -> int: return len(buf.lines) def buffer_line_len(buf: Buffer) -> int: return buffer_line_current(buf).length # oswinx, oswiny = 1940,50 oswinx, oswiny = 20,50 os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (oswinx,oswiny) os.environ['SDL_VIDEO_X11_WMCLASS'] = "pygame" clock = pg.time.Clock() pg.init() pg.font.init() text_renderer = pg.font.Font('JetBrainsMono-Regular.otf', 14) screen = pg.display.set_mode((900, 900), pg.RESIZABLE) pg.key.set_repeat(240, 10) def load_file(path): with open(path, 'r', encoding='utf-8') as f: text = f.read() f.close() return text @dataclass class GlobalContext: text: str current_buffer: Buffer cursor: Cursor # global ctx global ctx cursor = Cursor(width=8, height=20) if len(sys.argv) <= 1: ctx = GlobalContext("", buffer_create("", cursor), cursor) else: text = load_file(sys.argv[1]) ctx = GlobalContext(text, buffer_create(text, sys.argv[1], cursor), cursor) from config import * from editing import * from keybindings import * from treesitter import * def main(): running = True frame_count = 0 cursor_flash = 0 cursor_on = True line_height = 20 cursor = ctx.cursor current_buffer = ctx.current_buffer console = InteractiveInterpreter(locals()) console.runsource('from editing import *') # We are hardcoding precedence here while running: dt = clock.tick(120) / 1000.0 screen.fill('white') frame_count += 1 for event in pg.event.get(): # print(pg.event.event_name(event.type)) if event.type == pg.QUIT: running = False if event.type == pg.KEYDOWN: # print(pg.key.name(event.key)) cursor_on = True cursor_flash = 0 curr_line = buffer_line_current(current_buffer) kb_to_test = Keybinding(pg.key.name(event.key), event.mod) # print(kb_to_test, event.key, pg.key.name(event.key)) passthrough = True for i in range(len(keymap_stack) - 1, -1, -1): keymap = keymap_stack[i] if kb_to_test in keymap.mapping: keymap.mapping[kb_to_test]() if not keymap.pinned: keymap_stack.pop() passthrough = False break else: if keymap.pop_not_found: keymap_stack.pop() if not keymap.fallthrough: passthrough = False if len(kb_to_test.key) == 1: print(f"{kb_to_test} not found") break # No keybinding found so send the directly to the buffer # I think emacs instead binds every key to a function that sends the # letter, we can evaluate if that's better later on if passthrough and event.mod < 2 and event.unicode: buffer_insert_char(event.unicode) # if event.key == pg.K_RETURN: # src = buffer_line_current(current_buffer).piece.get_text() # try: # eval(src[3:]) # except: # print('error evaluating line: ' + src[3:]) # console.runsource(src[3:]) cur_col = 0 for i,curr_line in enumerate(current_buffer.lines): line_txt = curr_line.piece.get_text() ts_nodes = get_ts_nodes(curr_line.piece.get_text()) color_ranges = [(0, len(line_txt), 'black')] # color_ranges = [] for node in ts_nodes: if node.grammar_name == 'string_literal': start_col, end_col = node.start_point[1], node.end_point[1] print(node.start_point, node.end_point, node.text) prev_start,prev_end,prev_col = color_ranges[-1] color = 'green' if start_col > 0: color_ranges[-1] = (prev_start, start_col - 1,prev_col) color_ranges.append((start_col, end_col, color)) if end_col < len(line_txt): color_ranges.append((end_col + 1, end_col, color)) else: color_ranges[0] = (start_col, end_col, color) char_width = 8 for start,end,color in color_ranges: render = text_renderer.render(line_txt[start:end], True, color) screen.blit(render, (start * char_width, i * line_height)) continue # We need to iterate over the treesitter tree, I think that for now # it can be as easy as going until you find the 'string' grammar name # So we need to check three conditions to decide when to draw # 1: Did we hit a newline? # 2: Did we hit EOF? # 3: Did we run into a string # We need to figure out a way to ask treesitter if some # tokens are located at the current line that you're # checking. Since we're operating line by line. Since we # don't iterate character by character, we can't check if # we ran into a single or double quote which I guess we # could then use treesitter to part. It doesn't make sense # to do that anyway because while that might solve this # string task, ideally it's generalized so that as we walk # the lines we're rendering, we're also checking # treesitter grammar. But this is tricky because # treesitter doesn't do anything line by line. # So is what we're doing compatible then? maybe we should # consider iterating over the lines in a different way and # our current data structure won't scale at all. Maybe we # should just use the piece table wholesale after all and # not use insert mode to generate a temp buffer? That # sounds like it'll complicate things a lot. # Another thing would be to collect all the needed tokens? line_syntax = syntax_highlights.get_line(i) for grammar in line_syntax: if grammar.name == 'string': color = 'green' else: color = 'black' string = curr_line.piece.string_at(grammar.start, grammar.end - grammar.start) render = text_renderer.render(string, True, color) screen.blit(render, (0, i * line_height)) if cursor_on and cursor_flash < 0.5: cursor_draw(cursor) elif cursor_on: cursor_on = False cursor_flash = 0 elif not cursor_on and cursor_flash > 0.5: cursor_on = True cursor_flash = 0 cursor_flash += dt pg.display.flip() if __name__ == '__main__': main()