ruvim 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/ruvim/screen.rb CHANGED
@@ -19,17 +19,23 @@ module RuVim
19
19
  text_cols = [text_cols, 1].max
20
20
 
21
21
  rects = window_rects(editor, text_rows:, text_cols:)
22
+ if (current_rect = rects[editor.current_window_id])
23
+ editor.current_window_view_height_hint = [current_rect[:height].to_i, 1].max
24
+ end
22
25
  editor.window_order.each do |win_id|
23
26
  win = editor.windows.fetch(win_id)
24
27
  buf = editor.buffers.fetch(win.buffer_id)
25
28
  rect = rects[win_id]
26
29
  next unless rect
27
30
  content_width = [rect[:width] - number_column_width(editor, win, buf), 1].max
31
+ win.col_offset = 0 if wrap_enabled?(editor, win, buf)
28
32
  win.ensure_visible(
29
33
  buf,
30
34
  height: [rect[:height], 1].max,
31
35
  width: content_width,
32
- tabstop: tabstop_for(editor, win, buf)
36
+ tabstop: tabstop_for(editor, win, buf),
37
+ scrolloff: editor.effective_option("scrolloff", window: win, buffer: buf),
38
+ sidescrolloff: editor.effective_option("sidescrolloff", window: win, buffer: buf)
33
39
  )
34
40
  end
35
41
 
@@ -52,7 +58,9 @@ module RuVim
52
58
  text_rows = [text_rows, 1].max
53
59
  text_cols = [text_cols, 1].max
54
60
  rect = window_rects(editor, text_rows:, text_cols:)[editor.current_window_id]
55
- [rect ? rect[:height].to_i : text_rows, 1].max
61
+ height = [rect ? rect[:height].to_i : text_rows, 1].max
62
+ editor.current_window_view_height_hint = height if editor.respond_to?(:current_window_view_height_hint=)
63
+ height
56
64
  rescue StandardError
57
65
  1
58
66
  end
@@ -65,6 +73,7 @@ module RuVim
65
73
 
66
74
  status_row = text_rows + 1
67
75
  lines[status_row] = "\e[7m#{truncate(status_line(editor, cols), cols)}\e[m"
76
+ lines[status_row + 1] = ""
68
77
 
69
78
  if editor.command_line_active?
70
79
  cmd = editor.command_line
@@ -96,21 +105,15 @@ module RuVim
96
105
  rect = rects[win_id]
97
106
  next unless rect
98
107
 
99
- window = editor.windows.fetch(win_id)
100
- buffer = editor.buffers.fetch(window.buffer_id)
101
- gutter_w = number_column_width(editor, window, buffer)
102
- content_w = [rect[:width] - gutter_w, 1].max
103
- rect[:height].times do |dy|
104
- row_no = rect[:top] + dy
105
- buffer_row = window.row_offset + dy
106
- text =
107
- if buffer_row < buffer.line_count
108
- render_window_row(editor, window, buffer, buffer_row, gutter_w:, content_w:)
109
- else
110
- line_number_prefix(editor, window, buffer, nil, gutter_w) + pad_plain_display("~", content_w)
111
- end
112
- lines[row_no] = text
113
- end
108
+ window = editor.windows.fetch(win_id)
109
+ buffer = editor.buffers.fetch(window.buffer_id)
110
+ gutter_w = number_column_width(editor, window, buffer)
111
+ content_w = [rect[:width] - gutter_w, 1].max
112
+ rows = window_render_rows(editor, window, buffer, height: rect[:height], gutter_w:, content_w:)
113
+ rect[:height].times do |dy|
114
+ row_no = rect[:top] + dy
115
+ lines[row_no] = rows[dy] || (" " * rect[:width])
116
+ end
114
117
  end
115
118
 
116
119
  rects.each_value do |rect|
@@ -132,12 +135,12 @@ module RuVim
132
135
  dy = row_no - rect[:top]
133
136
  text =
134
137
  if dy >= 0 && dy < rect[:height]
135
- buffer_row = window.row_offset + dy
136
- if buffer_row < buffer.line_count
137
- render_window_row(editor, window, buffer, buffer_row, gutter_w:, content_w:)
138
- else
139
- line_number_prefix(editor, window, buffer, nil, gutter_w) + pad_plain_display("~", content_w)
140
- end
138
+ @__window_rows_cache ||= {}
139
+ key = [window.id, rect[:height], gutter_w, content_w, window.row_offset, window.col_offset, window.cursor_y, window.cursor_x,
140
+ editor.effective_option("wrap", window:, buffer:), editor.effective_option("linebreak", window:, buffer:),
141
+ editor.effective_option("breakindent", window:, buffer:), editor.effective_option("showbreak", window:, buffer:)]
142
+ @__window_rows_cache[key] ||= window_render_rows(editor, window, buffer, height: rect[:height], gutter_w:, content_w:)
143
+ @__window_rows_cache[key][dy] || (" " * rect[:width])
141
144
  else
142
145
  " " * rect[:width]
143
146
  end
@@ -146,6 +149,7 @@ module RuVim
146
149
  end
147
150
  lines[row_no] = pieces
148
151
  end
152
+ @__window_rows_cache = nil
149
153
  end
150
154
 
151
155
  def can_diff_render?(frame)
@@ -184,39 +188,268 @@ module RuVim
184
188
  def render_text_line(text, editor, buffer_row:, window:, buffer:, width:)
185
189
  tabstop = tabstop_for(editor, window, buffer)
186
190
  cells, display_col = RuVim::TextMetrics.clip_cells_for_width(text, width, source_col_start: window.col_offset, tabstop:)
191
+ render_cells(cells, display_col, editor, buffer_row:, window:, buffer:, width:, source_line: buffer.line_at(buffer_row),
192
+ source_col_offset: window.col_offset, leading_display_prefix: "")
193
+ end
194
+
195
+ def render_text_segment(source_line, editor, buffer_row:, window:, buffer:, width:, source_col_start:, display_prefix: "")
196
+ tabstop = tabstop_for(editor, window, buffer)
197
+ prefix = display_prefix.to_s
198
+ prefix_w = RuVim::DisplayWidth.display_width(prefix, tabstop:)
199
+ avail = [width - prefix_w, 0].max
200
+ cells, display_col = RuVim::TextMetrics.clip_cells_for_width(source_line[source_col_start..].to_s, avail, source_col_start:, tabstop:)
201
+ body = render_cells(cells, display_col, editor, buffer_row:, window:, buffer:, width: avail, source_line: source_line,
202
+ source_col_offset: source_col_start, leading_display_prefix: prefix)
203
+ if width <= 0
204
+ ""
205
+ elsif prefix_w <= 0
206
+ body
207
+ else
208
+ prefix_render = RuVim::TextMetrics.pad_plain_to_screen_width(prefix, [width, 0].max, tabstop:)[0...prefix.length].to_s
209
+ # body already includes padding for avail; prepend the visible prefix and trim to width.
210
+ out = prefix_render + body
211
+ out
212
+ end
213
+ end
214
+
215
+ def render_cells(cells, display_col, editor, buffer_row:, window:, buffer:, width:, source_line:, source_col_offset:, leading_display_prefix:)
187
216
  highlighted = +""
217
+ tabstop = tabstop_for(editor, window, buffer)
188
218
  visual = (editor.current_window_id == window.id && editor.visual_active?) ? editor.visual_selection(window) : nil
189
- search_cols = search_highlight_source_cols(editor, text, source_col_offset: window.col_offset)
190
- syntax_cols = syntax_highlight_source_cols(editor, window, buffer, text, source_col_offset: window.col_offset)
191
-
192
- cells.each_with_index do |cell, idx|
193
- ch = cell.glyph
219
+ text_for_highlight = source_line[source_col_offset..].to_s
220
+ search_cols = search_highlight_source_cols(editor, text_for_highlight, source_col_offset: source_col_offset)
221
+ syntax_cols = syntax_highlight_source_cols(editor, window, buffer, text_for_highlight, source_col_offset: source_col_offset)
222
+ list_enabled = !!editor.effective_option("list", window:, buffer:)
223
+ listchars = parse_listchars(editor.effective_option("listchars", window:, buffer:))
224
+ tab_seen = {}
225
+ trail_from = source_line.rstrip.length
226
+ cursorline = !!editor.effective_option("cursorline", window:, buffer:)
227
+ current_line = (editor.current_window_id == window.id && window.cursor_y == buffer_row)
228
+ cursorline_enabled = cursorline && current_line
229
+ colorcolumns = colorcolumn_display_cols(editor, window, buffer)
230
+ leading_prefix_width = RuVim::DisplayWidth.display_width(leading_display_prefix.to_s, tabstop:)
231
+ display_pos = leading_prefix_width
232
+
233
+ cells.each do |cell|
234
+ ch = display_glyph_for_cell(cell, source_line, list_enabled:, listchars:, tab_seen:, trail_from:)
194
235
  buffer_col = cell.source_col
195
236
  selected = selected_in_visual?(visual, buffer_row, buffer_col)
196
237
  cursor_here = (editor.current_window_id == window.id && window.cursor_y == buffer_row && window.cursor_x == buffer_col)
197
- if selected || cursor_here
238
+ colorcolumn_here = colorcolumns[display_pos]
239
+ if cursor_here
240
+ highlighted << cursor_cell_render(editor, ch)
241
+ elsif selected
198
242
  highlighted << "\e[7m#{ch}\e[m"
199
243
  elsif search_cols[buffer_col]
200
- highlighted << "\e[43m#{ch}\e[m"
244
+ highlighted << "#{search_bg_seq(editor)}#{ch}\e[m"
245
+ elsif colorcolumn_here
246
+ highlighted << "#{colorcolumn_bg_seq(editor)}#{ch}\e[m"
247
+ elsif cursorline_enabled
248
+ highlighted << "#{cursorline_bg_seq(editor)}#{ch}\e[m"
201
249
  elsif (syntax_color = syntax_cols[buffer_col])
202
250
  highlighted << "#{syntax_color}#{ch}\e[m"
203
251
  else
204
252
  highlighted << ch
205
253
  end
254
+ display_pos += [cell.display_width.to_i, 1].max
206
255
  end
207
256
 
208
257
  if editor.current_window_id == window.id && window.cursor_y == buffer_row
209
- col = window.cursor_x - window.col_offset
210
- if col >= cells.length && col >= 0 && display_col < width
211
- highlighted << "\e[7m \e[m"
258
+ cursor_target = virtual_cursor_display_pos(source_line, window.cursor_x, source_col_offset:, tabstop:, leading_prefix_width:)
259
+ if cursor_target && cursor_target >= display_pos && cursor_target < width
260
+ gap = cursor_target - display_pos
261
+ if gap.positive?
262
+ highlighted << (" " * gap)
263
+ display_col += gap
264
+ display_pos += gap
265
+ end
266
+ highlighted << cursor_cell_render(editor, " ")
212
267
  display_col += 1
268
+ display_pos += 1
213
269
  end
214
270
  end
215
271
 
216
- highlighted << (" " * [width - display_col, 0].max)
272
+ trailing = [width - display_col, 0].max
273
+ if trailing.positive? && cursorline_enabled
274
+ trailing.times do
275
+ if colorcolumns[display_pos]
276
+ highlighted << "#{colorcolumn_bg_seq(editor)} \e[m"
277
+ else
278
+ highlighted << "#{cursorline_bg_seq(editor)} \e[m"
279
+ end
280
+ display_pos += 1
281
+ end
282
+ else
283
+ highlighted << (" " * trailing)
284
+ end
217
285
  highlighted
218
286
  end
219
287
 
288
+ def virtual_cursor_display_pos(source_line, cursor_x, source_col_offset:, tabstop:, leading_prefix_width:)
289
+ return nil if cursor_x < source_col_offset
290
+
291
+ base = RuVim::TextMetrics.screen_col_for_char_index(source_line, cursor_x, tabstop:) -
292
+ RuVim::TextMetrics.screen_col_for_char_index(source_line, source_col_offset, tabstop:)
293
+ extra = [cursor_x.to_i - source_line.to_s.length, 0].max
294
+ leading_prefix_width + [base, 0].max + extra
295
+ end
296
+
297
+ def window_render_rows(editor, window, buffer, height:, gutter_w:, content_w:)
298
+ return plain_window_render_rows(editor, window, buffer, height:, gutter_w:, content_w:) unless wrap_enabled?(editor, window, buffer)
299
+
300
+ wrapped_window_render_rows(editor, window, buffer, height:, gutter_w:, content_w:)
301
+ end
302
+
303
+ def plain_window_render_rows(editor, window, buffer, height:, gutter_w:, content_w:)
304
+ Array.new(height) do |dy|
305
+ buffer_row = window.row_offset + dy
306
+ if buffer_row < buffer.line_count
307
+ render_window_row(editor, window, buffer, buffer_row, gutter_w:, content_w:)
308
+ else
309
+ line_number_prefix(editor, window, buffer, nil, gutter_w) + pad_plain_display("~", content_w)
310
+ end
311
+ end
312
+ end
313
+
314
+ def wrapped_window_render_rows(editor, window, buffer, height:, gutter_w:, content_w:)
315
+ rows = []
316
+ row_idx = window.row_offset
317
+ while rows.length < height
318
+ if row_idx >= buffer.line_count
319
+ rows << (line_number_prefix(editor, window, buffer, nil, gutter_w) + pad_plain_display("~", content_w))
320
+ next
321
+ end
322
+
323
+ line = buffer.line_at(row_idx)
324
+ segments = wrapped_segments_for_line(editor, window, buffer, line, width: content_w)
325
+ segments.each_with_index do |seg, seg_i|
326
+ break if rows.length >= height
327
+
328
+ gutter = line_number_prefix(editor, window, buffer, seg_i.zero? ? row_idx : nil, gutter_w)
329
+ rows << gutter + render_text_segment(line, editor, buffer_row: row_idx, window:, buffer:, width: content_w,
330
+ source_col_start: seg[:source_col_start], display_prefix: seg[:display_prefix])
331
+ end
332
+ row_idx += 1
333
+ end
334
+ rows
335
+ end
336
+
337
+ def wrap_enabled?(editor, window, buffer)
338
+ !!editor.effective_option("wrap", window:, buffer:)
339
+ end
340
+
341
+ def wrapped_segments_for_line(editor, window, buffer, line, width:)
342
+ return [{ source_col_start: 0, display_prefix: "" }] if width <= 0
343
+
344
+ tabstop = tabstop_for(editor, window, buffer)
345
+ linebreak = !!editor.effective_option("linebreak", window:, buffer:)
346
+ showbreak = editor.effective_option("showbreak", window:, buffer:).to_s
347
+ breakindent = !!editor.effective_option("breakindent", window:, buffer:)
348
+ indent_prefix = breakindent ? wrapped_indent_prefix(line, tabstop:, max_width: [width - RuVim::DisplayWidth.display_width(showbreak, tabstop:), 0].max) : ""
349
+
350
+ segs = []
351
+ start_col = 0
352
+ first = true
353
+ line = line.to_s
354
+ if line.empty?
355
+ return [{ source_col_start: 0, display_prefix: "" }]
356
+ end
357
+
358
+ while start_col < line.length
359
+ display_prefix = first ? "" : "#{showbreak}#{indent_prefix}"
360
+ prefix_w = RuVim::DisplayWidth.display_width(display_prefix, tabstop:)
361
+ avail = [width - prefix_w, 1].max
362
+ cells, = RuVim::TextMetrics.clip_cells_for_width(line[start_col..].to_s, avail, source_col_start: start_col, tabstop:)
363
+ if cells.empty?
364
+ segs << { source_col_start: start_col, display_prefix: display_prefix }
365
+ break
366
+ end
367
+
368
+ if linebreak && cells.length > 1
369
+ break_idx = linebreak_break_index(cells, line)
370
+ if break_idx && break_idx < cells.length - 1
371
+ cells = cells[0..break_idx]
372
+ end
373
+ end
374
+
375
+ segs << { source_col_start: start_col, display_prefix: display_prefix }
376
+ next_start = cells.last.source_col.to_i + 1
377
+ if linebreak
378
+ next_start += 1 while next_start < line.length && line[next_start] == " "
379
+ end
380
+ break if next_start <= start_col
381
+
382
+ start_col = next_start
383
+ first = false
384
+ end
385
+
386
+ segs
387
+ end
388
+
389
+ def linebreak_break_index(cells, line)
390
+ idx = nil
391
+ cells.each_with_index do |cell, i|
392
+ ch = line[cell.source_col]
393
+ idx = i if ch =~ /\s/
394
+ end
395
+ idx
396
+ end
397
+
398
+ def wrapped_indent_prefix(line, tabstop:, max_width:)
399
+ indent = line.to_s[/\A[ \t]*/].to_s
400
+ return "" if indent.empty? || max_width <= 0
401
+
402
+ RuVim::TextMetrics.pad_plain_to_screen_width(indent, max_width, tabstop:)[0...indent.length].to_s
403
+ rescue StandardError
404
+ ""
405
+ end
406
+
407
+ def display_glyph_for_cell(cell, source_line, list_enabled:, listchars:, tab_seen:, trail_from:)
408
+ return cell.glyph unless list_enabled
409
+
410
+ src = source_line[cell.source_col]
411
+ case src
412
+ when "\t"
413
+ first = !tab_seen[cell.source_col]
414
+ tab_seen[cell.source_col] = true
415
+ first ? listchars[:tab_head] : listchars[:tab_fill]
416
+ when " "
417
+ cell.source_col >= trail_from ? listchars[:trail] : cell.glyph
418
+ when "\u00A0"
419
+ listchars[:nbsp]
420
+ else
421
+ cell.glyph
422
+ end
423
+ end
424
+
425
+ def parse_listchars(raw)
426
+ raw_key = raw.to_s
427
+ @listchars_cache ||= {}
428
+ return @listchars_cache[raw_key] if @listchars_cache.key?(raw_key)
429
+
430
+ cfg = { tab_head: ">", tab_fill: "-", trail: "-", nbsp: "+" }
431
+ raw_key.split(",").each do |entry|
432
+ entry_key, val = entry.split(":", 2)
433
+ next unless entry_key && val
434
+
435
+ case entry_key.strip
436
+ when "tab"
437
+ chars = val.to_s.each_char.to_a
438
+ cfg[:tab_head] = chars[0] if chars[0]
439
+ cfg[:tab_fill] = chars[1] if chars[1]
440
+ when "trail"
441
+ ch = val.to_s.each_char.first
442
+ cfg[:trail] = ch if ch
443
+ when "nbsp"
444
+ ch = val.to_s.each_char.first
445
+ cfg[:nbsp] = ch if ch
446
+ end
447
+ end
448
+ @listchars_cache[raw_key] = cfg.freeze
449
+ rescue StandardError
450
+ { tab_head: ">", tab_fill: "-", trail: "-", nbsp: "+" }
451
+ end
452
+
220
453
  def render_window_row(editor, window, buffer, buffer_row, gutter_w:, content_w:)
221
454
  line = buffer.line_at(buffer_row)
222
455
  line = line[window.col_offset..] || ""
@@ -234,18 +467,24 @@ module RuVim
234
467
  end
235
468
 
236
469
  def number_column_width(editor, window, buffer)
470
+ sign_w = sign_column_width(editor, window, buffer)
237
471
  enabled = editor.effective_option("number", window:, buffer:) || editor.effective_option("relativenumber", window:, buffer:)
238
- return 0 unless enabled
472
+ return sign_w unless enabled
239
473
 
240
- [buffer.line_count.to_s.length, 1].max + 1
474
+ base = [buffer.line_count.to_s.length, 1].max
475
+ minw = editor.effective_option("numberwidth", window:, buffer:).to_i
476
+ sign_w + ([[base, minw].max, 1].max + 1)
241
477
  end
242
478
 
243
479
  def line_number_prefix(editor, window, buffer, buffer_row, width)
244
480
  return "" if width <= 0
481
+ sign_w = sign_column_width(editor, window, buffer)
482
+ sign = " " * sign_w
483
+ num_width = [width - sign_w, 0].max
245
484
  show_abs = editor.effective_option("number", window:, buffer:)
246
485
  show_rel = editor.effective_option("relativenumber", window:, buffer:)
247
- return " " * width unless show_abs || show_rel
248
- return " " * (width - 1) + " " if buffer_row.nil?
486
+ return sign + (" " * num_width) unless show_abs || show_rel
487
+ return sign + (" " * num_width) if buffer_row.nil?
249
488
 
250
489
  num =
251
490
  if show_rel && buffer_row != window.cursor_y
@@ -255,13 +494,72 @@ module RuVim
255
494
  else
256
495
  "0"
257
496
  end
258
- num.rjust(width - 1) + " "
497
+ sign + num.rjust([num_width - 1, 0].max) + (num_width.positive? ? " " : "")
498
+ end
499
+
500
+ def sign_column_width(editor, window, buffer)
501
+ raw = editor.effective_option("signcolumn", window:, buffer:).to_s
502
+ case raw
503
+ when "", "auto", "number"
504
+ 0
505
+ when "no"
506
+ 0
507
+ else
508
+ if (m = /\Ayes(?::(\d+))?\z/.match(raw))
509
+ n = m[1].to_i
510
+ n = 1 if n <= 0
511
+ n
512
+ else
513
+ 1
514
+ end
515
+ end
516
+ rescue StandardError
517
+ 0
518
+ end
519
+
520
+ def colorcolumn_display_cols(editor, window, buffer)
521
+ raw = editor.effective_option("colorcolumn", window:, buffer:).to_s
522
+ return {} if raw.empty?
523
+
524
+ @colorcolumn_cache ||= {}
525
+ return @colorcolumn_cache[raw] if @colorcolumn_cache.key?(raw)
526
+
527
+ cols = {}
528
+ raw.split(",").each do |tok|
529
+ t = tok.strip
530
+ next if t.empty?
531
+ next unless t.match?(/\A\d+\z/)
532
+ n = t.to_i
533
+ next if n <= 0
534
+ cols[n - 1] = true
535
+ end
536
+ @colorcolumn_cache[raw] = cols.freeze
537
+ rescue StandardError
538
+ {}
259
539
  end
260
540
 
261
541
  def pad_plain_display(text, width)
262
542
  RuVim::TextMetrics.pad_plain_to_screen_width(text, width, tabstop: DEFAULT_TABSTOP)
263
543
  end
264
544
 
545
+ def search_bg_seq(editor)
546
+ truecolor_enabled?(editor) ? "\e[48;2;255;215;0m" : "\e[43m"
547
+ end
548
+
549
+ def colorcolumn_bg_seq(editor)
550
+ truecolor_enabled?(editor) ? "\e[48;2;72;72;72m" : "\e[48;5;238m"
551
+ end
552
+
553
+ def cursorline_bg_seq(editor)
554
+ truecolor_enabled?(editor) ? "\e[48;2;58;58;58m" : "\e[48;5;236m"
555
+ end
556
+
557
+ def truecolor_enabled?(editor)
558
+ !!editor.effective_option("termguicolors")
559
+ rescue StandardError
560
+ false
561
+ end
562
+
265
563
  def status_line(editor, width)
266
564
  buffer = editor.current_buffer
267
565
  window = editor.current_window
@@ -275,14 +573,9 @@ module RuVim
275
573
  end
276
574
 
277
575
  path = buffer.display_name
278
- ft = editor.effective_option("filetype", buffer:, window:) || File.extname(buffer.path.to_s).delete_prefix(".")
279
- ft = "-" if ft.empty?
280
576
  mod = buffer.modified? ? " [+]" : ""
281
577
  msg = editor.message_error? ? "" : editor.message.to_s
282
- win_idx = (editor.window_order.index(editor.current_window_id) || 0) + 1
283
- win_total = editor.window_order.length
284
- tab_info = "t#{editor.current_tabpage_number}/#{editor.tabpage_count}"
285
- left = "#{mode} #{tab_info} w#{win_idx}/#{win_total} b#{buffer.id} #{path} [ft=#{ft}]#{mod}"
578
+ left = "#{mode} #{path}#{mod}"
286
579
  right = " #{window.cursor_y + 1}:#{window.cursor_x + 1} "
287
580
  body_width = [width - right.length, 0].max
288
581
  "#{compose_status_body(left, msg, body_width)}#{right}"
@@ -303,13 +596,21 @@ module RuVim
303
596
  end
304
597
 
305
598
  def truncate(str, width)
306
- str.to_s.ljust(width)[0, width]
599
+ RuVim::TextMetrics.terminal_safe_text(str).ljust(width)[0, width]
307
600
  end
308
601
 
309
602
  def error_message_line(msg, cols)
310
603
  "\e[97;41m#{truncate(msg, cols)}\e[m"
311
604
  end
312
605
 
606
+ def cursor_cell_render(editor, ch)
607
+ "#{cursor_cell_seq(editor)}#{ch}\e[m"
608
+ end
609
+
610
+ def cursor_cell_seq(editor)
611
+ "\e[7m"
612
+ end
613
+
313
614
  def cursor_screen_position(editor, text_rows, rects)
314
615
  window = editor.current_window
315
616
 
@@ -320,13 +621,47 @@ module RuVim
320
621
  end
321
622
 
322
623
  rect = rects[window.id] || { top: 1, left: 1 }
323
- row = rect[:top] + (window.cursor_y - window.row_offset)
324
- line = editor.current_buffer.line_at(window.cursor_y)
325
- gutter_w = number_column_width(editor, window, editor.current_buffer)
326
- tabstop = tabstop_for(editor, window, editor.current_buffer)
327
- prefix_screen_col = RuVim::TextMetrics.screen_col_for_char_index(line, window.cursor_x, tabstop:) -
328
- RuVim::TextMetrics.screen_col_for_char_index(line, window.col_offset, tabstop:)
329
- col = rect[:left] + gutter_w + [prefix_screen_col, 0].max
624
+ buffer = editor.current_buffer
625
+ line = buffer.line_at(window.cursor_y)
626
+ gutter_w = number_column_width(editor, window, buffer)
627
+ content_w = [rect[:width] - gutter_w, 1].max
628
+ tabstop = tabstop_for(editor, window, buffer)
629
+ if wrap_enabled?(editor, window, buffer)
630
+ visual_rows_before = 0
631
+ row = window.row_offset
632
+ while row < window.cursor_y
633
+ visual_rows_before += wrapped_segments_for_line(editor, window, buffer, buffer.line_at(row), width: content_w).length
634
+ row += 1
635
+ end
636
+ segs = wrapped_segments_for_line(editor, window, buffer, line, width: content_w)
637
+ seg_index = 0
638
+ segs.each_with_index do |seg, i|
639
+ nxt = segs[i + 1]
640
+ if nxt.nil? || window.cursor_x < nxt[:source_col_start]
641
+ seg_index = i
642
+ break
643
+ end
644
+ end
645
+ seg = segs[seg_index] || { source_col_start: 0, display_prefix: "" }
646
+ row = rect[:top] + visual_rows_before + seg_index
647
+ seg_prefix_w = RuVim::DisplayWidth.display_width(seg[:display_prefix].to_s, tabstop:)
648
+ extra_virtual = [window.cursor_x - line.length, 0].max
649
+ cursor_sc = RuVim::TextMetrics.screen_col_for_char_index(line, window.cursor_x, tabstop:) + extra_virtual
650
+ seg_sc = RuVim::TextMetrics.screen_col_for_char_index(line, seg[:source_col_start], tabstop:)
651
+ col = rect[:left] + gutter_w + seg_prefix_w + [cursor_sc - seg_sc, 0].max
652
+ else
653
+ row = rect[:top] + (window.cursor_y - window.row_offset)
654
+ extra_virtual = [window.cursor_x - line.length, 0].max
655
+ prefix_screen_col = RuVim::TextMetrics.screen_col_for_char_index(line, window.cursor_x, tabstop:) -
656
+ RuVim::TextMetrics.screen_col_for_char_index(line, window.col_offset, tabstop:)
657
+ col = rect[:left] + gutter_w + [prefix_screen_col, 0].max + extra_virtual
658
+ end
659
+ min_row = [rect[:top].to_i, 1].max
660
+ max_row = [rect[:top].to_i + [rect[:height].to_i, 1].max - 1, min_row].max
661
+ min_col = [rect[:left].to_i, 1].max
662
+ max_col = [rect[:left].to_i + [rect[:width].to_i, 1].max - 1, min_col].max
663
+ row = [[row.to_i, min_row].max, max_row].min
664
+ col = [[col.to_i, min_col].max, max_col].min
330
665
  [row, col]
331
666
  end
332
667
 
@@ -76,6 +76,15 @@ module RuVim
76
76
  end
77
77
 
78
78
  w = RuVim::DisplayWidth.cell_width(ch, col: display_col, tabstop:)
79
+ if terminal_unsafe_control_char?(ch)
80
+ w = [w, 1].max
81
+ break if display_col + w > max_width
82
+
83
+ cells << Cell.new(glyph: terminal_safe_placeholder(ch), source_col:, display_width: w)
84
+ display_col += w
85
+ source_col += 1
86
+ next
87
+ end
79
88
  break if display_col + w > max_width
80
89
 
81
90
  cells << Cell.new(glyph: ch, source_col:, display_width: w)
@@ -92,5 +101,22 @@ module RuVim
92
101
  out << (" " * [width.to_i - used, 0].max)
93
102
  out
94
103
  end
104
+
105
+ def terminal_safe_text(text)
106
+ text.to_s.each_char.map { |ch| terminal_unsafe_control_char?(ch) ? terminal_safe_placeholder(ch) : ch }.join
107
+ end
108
+
109
+ def terminal_unsafe_control_char?(ch)
110
+ return false if ch.nil? || ch.empty? || ch == "\t"
111
+
112
+ code = ch.ord
113
+ (code >= 0x00 && code < 0x20) || code == 0x7F || (0x80..0x9F).cover?(code)
114
+ rescue StandardError
115
+ false
116
+ end
117
+
118
+ def terminal_safe_placeholder(_ch)
119
+ "?"
120
+ end
95
121
  end
96
122
  end
data/lib/ruvim/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RuVim
4
- VERSION = "0.1.0"
5
- end
4
+ VERSION = "0.2.0"
5
+ end