import json import os import random import string import requests import vim from . import diff BASE_URL = 'https://via.ce9e.org/hmsg/pad/' session = requests.Session() pads = {} def get_selection(window): row, col = window.cursor pos = sum([len(line) + 1 for line in window.buffer[:(row - 1)]]) + col return [pos, pos] def set_selection(window, selection): row, col = 0, selection[0] for line in window.buffer: if col > len(line): col -= len(line) + 1 row += 1 else: window.cursor = row + 1, col break class Pad: def __init__(self, buffer): self.id = ''.join(random.sample(string.hexdigits, 6)) self.buffer = buffer self.local_changes = [] self.staged_changes = [] self.old = self.get_text() self.last_event_id = 0 self.name = os.path.basename(buffer.name).removesuffix('.pad') self.url = BASE_URL + self.name def get_text(self): return '\n'.join(self.buffer[:]) def start_listen(self): vim.command( f'let g:pad_curl_{self.buffer.number} = job_start(' f'["curl", "-N", "{self.url}"], ' f'{{"out_cb": function("pad#on_channel", [{self.buffer.number}])}}' ')' ) def stop_listen(self): vim.command(f'call job_stop(g:pad_curl_{self.buffer.number})') def on_input(self): text = self.get_text() change = diff.diff(self.old, text, 3) diff.push_change(self.local_changes, change) self.old = text def send_changes(self): if self.staged_changes: # TODO: retry again later print('abort because of staged changes') else: self.staged_changes = self.local_changes self.local_changes = [] data = [self.id, 'changes', self.staged_changes] try: r = session.post(self.url, json=data) r.raise_for_status() except Exception as e: print(e) self.local_changes = [*self.staged_changes, *self.local_changes] self.staged_changes = [] def apply_changes(self, changes): if vim.current.window.buffer == self.buffer: selection = get_selection(vim.current.window) else: selection = None text = self.get_text() prior = text my_changes = [*self.staged_changes, *self.local_changes] for change in reversed(my_changes): text = diff.unapply(text, change, selection) for change in changes: text = diff.apply(text, change, selection) for change in my_changes: text = diff.apply(text, change, selection) if text != prior: self.buffer[:] = text.split('\n') if vim.current.window.buffer == self.buffer: set_selection(vim.current.window, selection) self.old = text def reset_modified(self): mod = bool(self.local_changes or self.staged_changes) self.buffer.options['modified'] = mod def optimize(self): text = self.get_text() my_changes = [*self.staged_changes, *self.local_changes] for change in reversed(my_changes): text = diff.unapply(text, change) change = diff.diff('', text, 3) data = [self.id, 'changes', [change]] session.put(self.url, json=data, headers={ 'Last-Event-ID': self.last_event_id }) def get_buffer(): return int(vim.eval('expand("")'), 10) def on_open(): i = get_buffer() pad = Pad(vim.buffers[i]) pads[i] = pad pad.start_listen() print(f'Connected to {pad.url}') def on_input(): pad = pads[get_buffer()] pad.on_input() def on_write(): pad = pads[get_buffer()] pad.send_changes() def on_close(): pad = pads.pop(get_buffer()) pad.stop_listen() def on_channel(i, msg): pad = pads[i] if msg.startswith('data: '): data = json.loads(msg.split(': ', 1)[1]) if data[1] == 'changes': if data[0] == pad.id: pad.staged_changes = [] else: pad.apply_changes(data[2]) pad.reset_modified() if (random.random() < 0.05): pad.optimize() elif msg.startswith('id: '): pad.last_event_id = msg.split(': ', 1)[1]