vim-pad

minimal etherpad alternative - vim plugin
git clone https://git.ce9e.org/vim-pad.git

commit
9216eaac551c7a549c428b35fcbba56c56bfc15d
parent
02f23f4a18ede2130d2b0df3d8af6ec838df2a56
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2024-09-06 12:05
lint: tabs to spaces

Diffstat

M python/pad/__init__.py 250 ++++++++++++++++++++++++++++++------------------------------
M python/pad/diff.py 146 ++++++++++++++++++++++++++++++------------------------------

2 files changed, 198 insertions, 198 deletions


diff --git a/python/pad/__init__.py b/python/pad/__init__.py

@@ -15,152 +15,152 @@ pads = {}
   15    15 
   16    16 
   17    17 def get_selection(window):
   18    -1 	row, col = window.cursor
   19    -1 	pos = sum([len(line) + 1 for line in window.buffer[:(row - 1)]]) + col
   20    -1 	return [pos, pos]
   -1    18     row, col = window.cursor
   -1    19     pos = sum([len(line) + 1 for line in window.buffer[:(row - 1)]]) + col
   -1    20     return [pos, pos]
   21    21 
   22    22 
   23    23 def set_selection(window, selection):
   24    -1 	row, col = 0, selection[0]
   25    -1 	for line in window.buffer:
   26    -1 		if col > len(line):
   27    -1 			col -= len(line) + 1
   28    -1 			row += 1
   29    -1 		else:
   30    -1 			window.cursor = row + 1, col
   31    -1 			break
   -1    24     row, col = 0, selection[0]
   -1    25     for line in window.buffer:
   -1    26         if col > len(line):
   -1    27             col -= len(line) + 1
   -1    28             row += 1
   -1    29         else:
   -1    30             window.cursor = row + 1, col
   -1    31             break
   32    32 
   33    33 
   34    34 class Pad:
   35    -1 	def __init__(self, buffer):
   36    -1 		self.id = ''.join(random.sample(string.hexdigits, 6))
   37    -1 		self.buffer = buffer
   38    -1 		self.local_changes = []
   39    -1 		self.staged_changes = []
   40    -1 		self.old = self.get_text()
   41    -1 		self.last_event_id = 0
   42    -1 
   43    -1 		self.name = os.path.basename(buffer.name).removesuffix('.pad')
   44    -1 		self.url = BASE_URL + self.name
   45    -1 
   46    -1 	def get_text(self):
   47    -1 		return '\n'.join(self.buffer[:])
   48    -1 
   49    -1 	def start_listen(self):
   50    -1 		vim.command(
   51    -1 			f'let g:pad_curl_{self.buffer.number} = job_start('
   52    -1 				f'["curl", "-N", "{self.url}"], '
   53    -1 				f'{{"out_cb": function("pad#on_channel", [{self.buffer.number}])}}'
   54    -1 			')'
   55    -1 		)
   56    -1 
   57    -1 	def stop_listen(self):
   58    -1 		vim.command(f'call job_stop(g:pad_curl_{self.buffer.number})')
   59    -1 
   60    -1 	def on_input(self):
   61    -1 		text = self.get_text()
   62    -1 		change = diff.diff(self.old, text, 3)
   63    -1 		diff.push_change(self.local_changes, change)
   64    -1 		self.old = text
   65    -1 
   66    -1 	def send_changes(self):
   67    -1 		if self.staged_changes:
   68    -1 			# TODO: retry again later
   69    -1 			print('abort because of staged changes')
   70    -1 		else:
   71    -1 			self.staged_changes = self.local_changes
   72    -1 			self.local_changes = []
   73    -1 			data = [self.id, 'changes', self.staged_changes]
   74    -1 			try:
   75    -1 				r = session.post(self.url, json=data)
   76    -1 				r.raise_for_status()
   77    -1 			except Exception as e:
   78    -1 				print(e)
   79    -1 				self.local_changes = [*self.staged_changes, *self.local_changes]
   80    -1 				self.staged_changes = []
   81    -1 
   82    -1 	def apply_changes(self, changes):
   83    -1 		if vim.current.window.buffer == self.buffer:
   84    -1 			selection = get_selection(vim.current.window)
   85    -1 		else:
   86    -1 			selection = None
   87    -1 		text = self.get_text()
   88    -1 		prior = text
   89    -1 		my_changes = [*self.staged_changes, *self.local_changes]
   90    -1 
   91    -1 		for change in reversed(my_changes):
   92    -1 			text = diff.unapply(text, change, selection)
   93    -1 
   94    -1 		for change in changes:
   95    -1 			text = diff.apply(text, change, selection)
   96    -1 
   97    -1 		for change in my_changes:
   98    -1 			text = diff.apply(text, change, selection)
   99    -1 
  100    -1 		if text != prior:
  101    -1 			self.buffer[:] = text.split('\n')
  102    -1 			if vim.current.window.buffer == self.buffer:
  103    -1 				set_selection(vim.current.window, selection)
  104    -1 			self.old = text
  105    -1 
  106    -1 	def reset_modified(self):
  107    -1 		mod = bool(self.local_changes or self.staged_changes)
  108    -1 		self.buffer.options['modified'] = mod
  109    -1 
  110    -1 	def optimize(self):
  111    -1 		text = self.get_text()
  112    -1 		my_changes = [*self.staged_changes, *self.local_changes]
  113    -1 
  114    -1 		for change in reversed(my_changes):
  115    -1 			text = diff.unapply(text, change)
  116    -1 
  117    -1 		change = diff.diff('', text, 3)
  118    -1 		data = [self.id, 'changes', [change]]
  119    -1 		session.put(self.url, json=data, headers={
  120    -1 			'Last-Event-ID': self.last_event_id
  121    -1 		})
   -1    35     def __init__(self, buffer):
   -1    36         self.id = ''.join(random.sample(string.hexdigits, 6))
   -1    37         self.buffer = buffer
   -1    38         self.local_changes = []
   -1    39         self.staged_changes = []
   -1    40         self.old = self.get_text()
   -1    41         self.last_event_id = 0
   -1    42 
   -1    43         self.name = os.path.basename(buffer.name).removesuffix('.pad')
   -1    44         self.url = BASE_URL + self.name
   -1    45 
   -1    46     def get_text(self):
   -1    47         return '\n'.join(self.buffer[:])
   -1    48 
   -1    49     def start_listen(self):
   -1    50         vim.command(
   -1    51             f'let g:pad_curl_{self.buffer.number} = job_start('
   -1    52                 f'["curl", "-N", "{self.url}"], '
   -1    53                 f'{{"out_cb": function("pad#on_channel", [{self.buffer.number}])}}'
   -1    54             ')'
   -1    55         )
   -1    56 
   -1    57     def stop_listen(self):
   -1    58         vim.command(f'call job_stop(g:pad_curl_{self.buffer.number})')
   -1    59 
   -1    60     def on_input(self):
   -1    61         text = self.get_text()
   -1    62         change = diff.diff(self.old, text, 3)
   -1    63         diff.push_change(self.local_changes, change)
   -1    64         self.old = text
   -1    65 
   -1    66     def send_changes(self):
   -1    67         if self.staged_changes:
   -1    68             # TODO: retry again later
   -1    69             print('abort because of staged changes')
   -1    70         else:
   -1    71             self.staged_changes = self.local_changes
   -1    72             self.local_changes = []
   -1    73             data = [self.id, 'changes', self.staged_changes]
   -1    74             try:
   -1    75                 r = session.post(self.url, json=data)
   -1    76                 r.raise_for_status()
   -1    77             except Exception as e:
   -1    78                 print(e)
   -1    79                 self.local_changes = [*self.staged_changes, *self.local_changes]
   -1    80                 self.staged_changes = []
   -1    81 
   -1    82     def apply_changes(self, changes):
   -1    83         if vim.current.window.buffer == self.buffer:
   -1    84             selection = get_selection(vim.current.window)
   -1    85         else:
   -1    86             selection = None
   -1    87         text = self.get_text()
   -1    88         prior = text
   -1    89         my_changes = [*self.staged_changes, *self.local_changes]
   -1    90 
   -1    91         for change in reversed(my_changes):
   -1    92             text = diff.unapply(text, change, selection)
   -1    93 
   -1    94         for change in changes:
   -1    95             text = diff.apply(text, change, selection)
   -1    96 
   -1    97         for change in my_changes:
   -1    98             text = diff.apply(text, change, selection)
   -1    99 
   -1   100         if text != prior:
   -1   101             self.buffer[:] = text.split('\n')
   -1   102             if vim.current.window.buffer == self.buffer:
   -1   103                 set_selection(vim.current.window, selection)
   -1   104             self.old = text
   -1   105 
   -1   106     def reset_modified(self):
   -1   107         mod = bool(self.local_changes or self.staged_changes)
   -1   108         self.buffer.options['modified'] = mod
   -1   109 
   -1   110     def optimize(self):
   -1   111         text = self.get_text()
   -1   112         my_changes = [*self.staged_changes, *self.local_changes]
   -1   113 
   -1   114         for change in reversed(my_changes):
   -1   115             text = diff.unapply(text, change)
   -1   116 
   -1   117         change = diff.diff('', text, 3)
   -1   118         data = [self.id, 'changes', [change]]
   -1   119         session.put(self.url, json=data, headers={
   -1   120             'Last-Event-ID': self.last_event_id
   -1   121         })
  122   122 
  123   123 
  124   124 def get_buffer():
  125    -1 	return int(vim.eval('expand("<abuf>")'), 10)
   -1   125     return int(vim.eval('expand("<abuf>")'), 10)
  126   126 
  127   127 
  128   128 def on_open():
  129    -1 	i = get_buffer()
  130    -1 	pad = Pad(vim.buffers[i])
  131    -1 	pads[i] = pad
  132    -1 	pad.start_listen()
  133    -1 	print(f'Connected to {pad.url}')
   -1   129     i = get_buffer()
   -1   130     pad = Pad(vim.buffers[i])
   -1   131     pads[i] = pad
   -1   132     pad.start_listen()
   -1   133     print(f'Connected to {pad.url}')
  134   134 
  135   135 
  136   136 def on_input():
  137    -1 	pad = pads[get_buffer()]
  138    -1 	pad.on_input()
   -1   137     pad = pads[get_buffer()]
   -1   138     pad.on_input()
  139   139 
  140   140 
  141   141 def on_write():
  142    -1 	pad = pads[get_buffer()]
  143    -1 	pad.send_changes()
   -1   142     pad = pads[get_buffer()]
   -1   143     pad.send_changes()
  144   144 
  145   145 
  146   146 def on_close():
  147    -1 	pad = pads.pop(get_buffer())
  148    -1 	pad.stop_listen()
   -1   147     pad = pads.pop(get_buffer())
   -1   148     pad.stop_listen()
  149   149 
  150   150 
  151   151 def on_channel(i, msg):
  152    -1 	pad = pads[i]
  153    -1 
  154    -1 	if msg.startswith('data: '):
  155    -1 		data = json.loads(msg.split(': ', 1)[1])
  156    -1 		if data[1] == 'changes':
  157    -1 			if data[0] == pad.id:
  158    -1 				pad.staged_changes = []
  159    -1 			else:
  160    -1 				pad.apply_changes(data[2])
  161    -1 			pad.reset_modified()
  162    -1 
  163    -1 			if (random.random() < 0.05):
  164    -1 				pad.optimize()
  165    -1 	elif msg.startswith('id: '):
  166    -1 		pad.last_event_id = msg.split(': ', 1)[1]
   -1   152     pad = pads[i]
   -1   153 
   -1   154     if msg.startswith('data: '):
   -1   155         data = json.loads(msg.split(': ', 1)[1])
   -1   156         if data[1] == 'changes':
   -1   157             if data[0] == pad.id:
   -1   158                 pad.staged_changes = []
   -1   159             else:
   -1   160                 pad.apply_changes(data[2])
   -1   161             pad.reset_modified()
   -1   162 
   -1   163             if (random.random() < 0.05):
   -1   164                 pad.optimize()
   -1   165     elif msg.startswith('id: '):
   -1   166         pad.last_event_id = msg.split(': ', 1)[1]

diff --git a/python/pad/diff.py b/python/pad/diff.py

@@ -1,105 +1,105 @@
    1     1 def _slice(text, start, end):
    2    -1 	if end:
    3    -1 		return text[start:-end]
    4    -1 	else:
    5    -1 		return text[start:]
   -1     2     if end:
   -1     3         return text[start:-end]
   -1     4     else:
   -1     5         return text[start:]
    6     6 
    7     7 
    8     8 def _apply(text, change, selection=None):
    9    -1 	pos, before, after = change
   -1     9     pos, before, after = change
   10    10 
   11    -1 	if selection:
   12    -1 		d = diff(before, after, 0)
   13    -1 		if pos + d[0] <= selection[0]:
   14    -1 			selection[0] += len(after) - len(before)
   15    -1 		if pos + d[0] <= selection[1]:
   16    -1 			selection[1] += len(after) - len(before)
   -1    11     if selection:
   -1    12         d = diff(before, after, 0)
   -1    13         if pos + d[0] <= selection[0]:
   -1    14             selection[0] += len(after) - len(before)
   -1    15         if pos + d[0] <= selection[1]:
   -1    16             selection[1] += len(after) - len(before)
   17    17 
   18    -1 	return text[0:pos] + after + text[pos + len(before):]
   -1    18     return text[0:pos] + after + text[pos + len(before):]
   19    19 
   20    20 
   21    21 def apply(text, change, selection=None):
   22    -1 	start, end, before, after = change
   -1    22     start, end, before, after = change
   23    23 
   24    -1 	# special handling for resets
   25    -1 	if start == 0 and end == 0 and before == '' and text != '':
   26    -1 		start, end, before, after = diff(text, after, 3)
   -1    24     # special handling for resets
   -1    25     if start == 0 and end == 0 and before == '' and text != '':
   -1    26         start, end, before, after = diff(text, after, 3)
   27    27 
   28    -1 	# try exact match
   29    -1 	if text[start:].startswith(before):
   30    -1 		return _apply(text, [start, before, after], selection)
   -1    28     # try exact match
   -1    29     if text[start:].startswith(before):
   -1    30         return _apply(text, [start, before, after], selection)
   31    31 
   32    -1 	# try exact match in similar position
   33    -1 	best = -1
   34    -1 	best_dist = len(text)
   35    -1 	for i in range(len(text)):
   36    -1 		if not text[i:].startswith(before):
   37    -1 			continue
   38    -1 		dist = abs(i - start)
   39    -1 		if dist < best_dist:
   40    -1 			best = i
   41    -1 			best_dist = dist
   42    -1 		else:
   43    -1 			break
   -1    32     # try exact match in similar position
   -1    33     best = -1
   -1    34     best_dist = len(text)
   -1    35     for i in range(len(text)):
   -1    36         if not text[i:].startswith(before):
   -1    37             continue
   -1    38         dist = abs(i - start)
   -1    39         if dist < best_dist:
   -1    40             best = i
   -1    41             best_dist = dist
   -1    42         else:
   -1    43             break
   44    44 
   45    -1 	if best != -1:
   46    -1 		return _apply(text, [best, before, after], selection)
   -1    45     if best != -1:
   -1    46         return _apply(text, [best, before, after], selection)
   47    47 
   48    -1 	# otherwise, ignore the change
   49    -1 	return text
   -1    48     # otherwise, ignore the change
   -1    49     return text
   50    50 
   51    51 
   52    52 def unapply(text, change, selection=None):
   53    -1 	start, end, before, after = change
   54    -1 	return apply(text, [start, end, after, before], selection)
   -1    53     start, end, before, after = change
   -1    54     return apply(text, [start, end, after, before], selection)
   55    55 
   56    56 
   57    57 def diff(text1, text2, ctx):
   58    -1 	start = 0
   59    -1 	end = 0
   -1    58     start = 0
   -1    59     end = 0
   60    60 
   61    -1 	while (
   62    -1 		start < len(text1)
   63    -1 		and start < len(text2)
   64    -1 		and text1[start] == text2[start]
   65    -1 	):
   66    -1 		start += 1
   67    -1 	while (
   68    -1 		start + end < len(text1)
   69    -1 		and start + end < len(text2)
   70    -1 		and text1[-(end + 1)] == text2[-(end + 1)]
   71    -1 	):
   72    -1 		end += 1
   -1    61     while (
   -1    62         start < len(text1)
   -1    63         and start < len(text2)
   -1    64         and text1[start] == text2[start]
   -1    65     ):
   -1    66         start += 1
   -1    67     while (
   -1    68         start + end < len(text1)
   -1    69         and start + end < len(text2)
   -1    70         and text1[-(end + 1)] == text2[-(end + 1)]
   -1    71     ):
   -1    72         end += 1
   73    73 
   74    -1 	start = max(0, start - ctx)
   75    -1 	end = max(0, end - ctx)
   -1    74     start = max(0, start - ctx)
   -1    75     end = max(0, end - ctx)
   76    76 
   77    -1 	return [start, end, _slice(text1, start, end), _slice(text2, start, end)]
   -1    77     return [start, end, _slice(text1, start, end), _slice(text2, start, end)]
   78    78 
   79    79 
   80    80 def merge(change1, change2):
   81    -1 	start1, end1, before1, after1 = change1
   82    -1 	start2, end2, before2, after2 = change2
   -1    81     start1, end1, before1, after1 = change1
   -1    82     start2, end2, before2, after2 = change2
   83    83 
   84    -1 	if start1 + len(after1) + end1 == start2 + len(before2) + end2:
   85    -1 		# merge subsequent inserts
   86    -1 		if start2 >= start1 and after1[start2 - start1:].startswith(before2):
   87    -1 			after = _apply(after1, [start2 - start1, before2, after2])
   88    -1 			return [[start1, end1, before1, after]]
   -1    84     if start1 + len(after1) + end1 == start2 + len(before2) + end2:
   -1    85         # merge subsequent inserts
   -1    86         if start2 >= start1 and after1[start2 - start1:].startswith(before2):
   -1    87             after = _apply(after1, [start2 - start1, before2, after2])
   -1    88             return [[start1, end1, before1, after]]
   89    89 
   90    -1 		# merge subsequent deletes (inverse insert)
   91    -1 		if start1 >= start2 and before2[start1 - start2:].startswith(after1):
   92    -1 			before = _apply(before2, [start1 - start2, after1, before1])
   93    -1 			return [[start2, end2, before, after2]]
   -1    90         # merge subsequent deletes (inverse insert)
   -1    91         if start1 >= start2 and before2[start1 - start2:].startswith(after1):
   -1    92             before = _apply(before2, [start1 - start2, after1, before1])
   -1    93             return [[start2, end2, before, after2]]
   94    94 
   95    -1 	return [change1, change2]
   -1    95     return [change1, change2]
   96    96 
   97    97 
   98    98 def push_change(changes, change):
   99    -1 	if not changes:
  100    -1 		changes.append(change)
  101    -1 	else:
  102    -1 		last = changes.pop()
  103    -1 		merged = merge(last, change)
  104    -1 		for change in merged:
  105    -1 			changes.append(change)
   -1    99     if not changes:
   -1   100         changes.append(change)
   -1   101     else:
   -1   102         last = changes.pop()
   -1   103         merged = merge(last, change)
   -1   104         for change in merged:
   -1   105             changes.append(change)