godot-xterm/addons/godot_xterm/buffer/buffer.gd
2020-09-13 19:28:34 +02:00

430 lines
14 KiB
GDScript

# Copyright (c) 2017 The xterm.js authors. All rights reserved.
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends Reference
const BufferLine = preload("res://addons/godot_xterm/buffer/buffer_line.gd")
const CellData = preload("res://addons/godot_xterm/buffer/cell_data.gd")
const Charsets = preload("res://addons/godot_xterm/data/charsets.gd")
const Constants = preload("res://addons/godot_xterm/buffer/constants.gd")
const CircularList = preload("res://addons/godot_xterm/circular_list.gd")
const AttributeData = preload("res://addons/godot_xterm/buffer/attribute_data.gd")
const BufferReflow = preload("res://addons/godot_xterm/buffer/buffer_reflow.gd")
const MAX_BUFFER_SIZE = 4294967295 # 2^32 - 1
var lines
var ydisp: int = 0
var ybase: int = 0
var y: int = 0
var x: int = 0
var scroll_bottom: int
var scroll_top: int
var tabs = {}
var saved_y: int = 0
var saved_x: int = 0
var saved_cur_attr_data = AttributeData.new()
var saved_charset = Charsets.DEFAULT_CHARSET
var markers: Array = []
var _null_cell = CellData.from_char_data([0, Constants.NULL_CELL_CHAR,
Constants.NULL_CELL_WIDTH, Constants.NULL_CELL_CODE])
var _whitespace_cell = CellData.from_char_data([0, Constants.WHITESPACE_CELL_CHAR,
Constants.WHITESPACE_CELL_WIDTH, Constants.WHITESPACE_CELL_CODE])
var _cols: int
var _rows: int
var _has_scrollback
var _options_service
var _buffer_service
func _init(has_scrollback: bool, options_service, buffer_service):
_has_scrollback = has_scrollback
_options_service = options_service
_buffer_service = buffer_service
_cols = buffer_service.cols
_rows = buffer_service.rows
lines = CircularList.new(_get_correct_buffer_length(_rows))
scroll_top = 0
scroll_bottom = _rows - 1
setup_tab_stops()
# Resizes the buffer, adjusting its data accordingly.
# @param new_cols The new number of columns.
# @param new_rows The new number of rows.
func resize(new_cols: int, new_rows: int) -> void:
# store reference to null cell with default attrs
var null_cell = get_null_cell(AttributeData.new())
# Increase max length if needed before adjustments to allow space to fill
# as required.
var new_max_length = _get_correct_buffer_length(new_rows)
if new_max_length > lines.max_length:
lines.max_length = new_max_length
# The following adjustments should only happen if the buffer has been
# initialized/filled.
if lines.length > 0:
# Deal with columns increasing (reducing needs to happen after reflow)
if _cols < new_cols:
for i in range(lines.length):
lines.get_line(i).resize(new_cols, null_cell)
# Resize rows in both directions as needed
var add_to_y = 0
if _rows < new_rows:
for y in range(_rows, new_rows):
if lines.length < new_rows + ybase:
if _options_service.options.windows_mode:
# Just add the new missing rows on Windows as conpty reprints the screen with it's
# view of the world. Once a line enters scrollback for conpty it remains there
lines.push(BufferLine.new(new_cols, null_cell))
else:
if ybase > 0 and lines.length <= ybase + y + add_to_y + 1:
# There is room above the buffer and there are no empty elements below the line,
# scroll up
ybase -= 1
add_to_y += 1
if ydisp > 0:
# Viewport is at the top of the buffer, must increase downwards
ydisp -= 1
else:
# Add a blank line if tere is no buffer left at the top to srcoll to, or if there
# are blank lines after the cursor
lines.push(BufferLine.new(new_cols, null_cell))
else: # _rows >= new_rows
for _y in range(_rows, new_rows, -1):
if lines.length > new_rows + ybase:
if lines.length > ybase + y + 1:
# The line is blank line below the cursor, remove it
lines.pop()
else:
# The line is the cursor, scroll down
ybase += 1
ydisp += 1
# Reduce max length if needed after adjustments, this is done after as it
# would otherwise cut data from the bottom of the buffer.
if new_max_length < lines.max_length:
# Trim from the top of th ebuffer and adjust ybase and ydisp.
var amount_to_trim = lines.length - new_max_length
if amount_to_trim > 0:
lines.trim_start(amount_to_trim)
ybase = max(ybase - amount_to_trim, 0)
ydisp = max(ydisp - amount_to_trim, 0)
saved_y = max(saved_y - amount_to_trim, 0)
lines.max_length = new_max_length
# Make sure that the cursor stays on screen
x = min(x, new_cols - 1)
y = min(y, new_rows - 1)
if add_to_y:
y += add_to_y
saved_x = min(saved_x, new_cols -1)
scroll_top = 0
scroll_bottom = new_rows - 1
if _is_reflow_enabled():
_reflow(new_cols, new_rows)
# Trim the end of the line off if cols shrunk
if _cols > new_cols:
for i in range(lines.length):
lines.get_line(i).resize(new_cols, null_cell)
_cols = new_cols
_rows = new_rows
func _is_reflow_enabled() -> bool:
return _has_scrollback and not _options_service.options.windows_mode
func _reflow(new_cols: int, new_rows: int) -> void:
if _cols == new_cols:
return
# Iterate through rows, ignore the last one as it cannot be wrapped
if new_cols > _cols:
_reflow_larger(new_cols, new_rows)
else:
_reflow_smaller(new_cols, new_rows)
func _reflow_larger(new_cols: int, new_rows: int) -> void:
var to_remove: PoolIntArray = BufferReflow.reflow_larger_get_lines_to_remove(lines,
_cols, new_cols, ybase + y, get_null_cell(AttributeData.new()))
if not to_remove.empty():
var new_layout_result = BufferReflow.reflow_larger_create_new_layout(lines, to_remove)
BufferReflow.reflow_larger_apply_new_layout(lines, new_layout_result.layout)
_reflow_larger_adjust_viewport(new_cols, new_rows, new_layout_result.count_removed)
func _reflow_larger_adjust_viewport(new_cols: int, new_rows: int, count_removed: int) -> void:
var null_cell = get_null_cell(AttributeData.new())
# Adjust viewport based on number of items removed
var viewport_adjustments = count_removed
while viewport_adjustments > 0:
viewport_adjustments -= 1
if ybase == 0:
if y > 0:
y -= 1
if lines.length < new_rows:
# Add an extra row at the bottom of the viewport
lines.push(BufferLine.new(new_cols, null_cell))
else:
if ydisp == ybase:
ydisp -= 1
ybase -= 1
saved_y = max(saved_y - count_removed, 0)
func _reflow_smaller(new_cols: int, new_rows: int) -> void:
var null_cell = get_null_cell(AttributeData.new())
# Gather all BufferLines that need to be inserted into the Buffer here so that they can be
# batched up and only commited once
var to_insert = []
var count_to_insert = 0
# Go backwards as many lines may be trimmed and this will avoid considering them
var i = lines.length - 1
while i >= 0:
# Check wether this line is a problem
var next_line = lines.get_line(i)
if not next_line or not next_line.is_wrapped and next_line.get_trimmed_length() <= new_cols:
i -= 1
continue
# Gather wrapped lines and adjust y to be the starting line
var wrapped_lines = [next_line]
while next_line.is_wrapped and i > 0:
i -= 1
next_line = lines.get_line(i)
wrapped_lines.push_front(next_line)
# If these lines contain the cursor don't touch them, the program will handle fixing up
# wrapped lines with the cursor
var absolute_y = ybase + y
if absolute_y >= i and absolute_y < i + wrapped_lines.size():
i -= 1
continue
var last_line_length = wrapped_lines[wrapped_lines.size() - 1].get_trimmed_length()
var dest_line_lengths = BufferReflow.reflow_smaller_get_new_line_lengths(wrapped_lines, _cols, new_cols)
var lines_to_add = dest_line_lengths.size() - wrapped_lines.size()
var trimmed_lines: int
if ybase == 0 and y != lines.length - 1:
# If the top section of the buffer is not yet filled
trimmed_lines = max(0, y - lines.max_length + lines_to_add)
else:
trimmed_lines = max(0, lines.length - lines.max_length + lines_to_add)
# Add the new lines
var new_lines = []
for j in range(lines_to_add):
var new_line = get_blank_line(AttributeData.new(), true)
new_lines.append(new_line)
if not new_lines.empty():
to_insert.append({"start": i + wrapped_lines.size() + count_to_insert,
"new_lines": new_lines})
count_to_insert += new_lines.size()
wrapped_lines += new_lines
# Copy buffer data to new locations, this needs to happen backwards to do in-place
var dest_line_index = dest_line_lengths.size() - 1 # floor(cells_needed / new_cols)
var dest_col = dest_line_lengths[dest_line_index] # cells_needed % new_cols
if dest_col == 0:
dest_line_index -= 1
dest_col = dest_line_lengths[dest_line_index]
var src_line_index = wrapped_lines.size() - lines_to_add - 1
var src_col = last_line_length
while src_line_index >= 0:
var cells_to_copy = min(src_col, dest_col)
wrapped_lines[dest_line_index].copy_cells_from(wrapped_lines[src_line_index],
src_col - cells_to_copy, dest_col - cells_to_copy, cells_to_copy, true)
dest_col -= cells_to_copy
if dest_col == 0:
dest_line_index -= 1
dest_col = dest_line_lengths[dest_line_index]
src_col -= cells_to_copy
if src_col == 0:
src_line_index -= 1
var wrapped_lines_index = max(src_line_index, 0)
src_col = BufferReflow.get_wrapped_line_trimmed_length(wrapped_lines, wrapped_lines_index, _cols)
# Null out the end of the line ends if a wide character wrapped to the following line
for j in range(wrapped_lines.size()):
if dest_line_lengths[j] < new_cols:
wrapped_lines[j].set_cell(dest_line_lengths[j], null_cell)
# Adjust viewport as needed
var viewport_adjustments = lines_to_add - trimmed_lines
while viewport_adjustments > 0:
if ybase == 0:
if y < new_rows - 1:
y += 1
lines.pop()
else:
ybase += 1
ydisp += 1
else:
# Ensure ybase does not exceed its maximum value
if ybase < min(lines.max_length, (lines.length + count_to_insert) - new_rows):
if ybase == ydisp:
ydisp += 1
ybase += 1
viewport_adjustments -= 1
saved_y = min(saved_y + lines_to_add, ybase + new_rows - 1)
i -= 1
# Rearrange lines in the buffer if there are any insertions, this is done at the end rather
# than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
# costly calls to CircularList.splice.
if not to_insert.empty():
# Record buffer insert events and then play them backwards so that the indexes are
# correct
var insert_events = []
# Record original lines so they don't get overridden when we rearrange the list
var original_lines = []
for j in range(lines.length):
original_lines.append(lines.get_line(j))
var original_lines_length = lines.length
var original_line_index = original_lines_length - 1
var next_to_insert_index = 0
var next_to_insert = to_insert[next_to_insert_index]
lines.length = min(lines.max_length, lines.length + count_to_insert)
var count_inserted_so_far = 0
var j = min(lines.max_length - 1, original_lines_length + count_to_insert - 1)
while j >= 0:
if next_to_insert and next_to_insert.start > original_line_index + count_inserted_so_far:
# Insert extra lines here, adjusting i as needed
for next_i in range(next_to_insert.new_lines.size() - 1, -1, -1):
lines.set_line(j, next_to_insert.new_lines[next_i])
j -= 1
j += 1
# Create insert events for later
insert_events.append({"index": original_line_index + 1,
"amount": next_to_insert.new_lines.size()})
count_inserted_so_far += next_to_insert.new_lines.size()
next_to_insert_index += 1
next_to_insert = to_insert[next_to_insert_index] if to_insert.size() > next_to_insert_index else null
else:
lines.set_line(j, original_lines[original_line_index])
original_line_index -= 1
j -= 1
# Update markers
var insert_count_emitted = 0
for k in range(insert_events.size() - 1, -1, -1):
insert_events[k].index += insert_count_emitted
lines.emit_signal("inserted", insert_events[k])
insert_count_emitted += insert_events[k].amount
var amount_to_trim = max(0, original_lines_length + count_to_insert - lines.max_length)
if amount_to_trim > 0:
lines.emit_signal("trimmed", amount_to_trim)
func get_null_cell(attr = null):
if attr:
_null_cell.fg = attr.fg
_null_cell.bg = attr.bg
_null_cell.extended = attr.extended
else:
_null_cell.fg = 0
_null_cell.bg = 0
_null_cell.extended = AttributeData.ExtendedAttrs.new()
return _null_cell
func get_blank_line(attr, is_wrapped: bool = false):
return BufferLine.new(_buffer_service.cols, get_null_cell(attr), is_wrapped)
func _get_correct_buffer_length(rows: int) -> int:
if not _has_scrollback:
return rows
else:
var correct_buffer_length = rows + _options_service.options.scrollback
return correct_buffer_length if correct_buffer_length < MAX_BUFFER_SIZE else MAX_BUFFER_SIZE
# Fills the viewport with blank lines.
func fill_viewport_rows(fill_attr = null) -> void:
if lines.length == 0:
if not fill_attr:
fill_attr = AttributeData.new()
var i = _rows
while i:
lines.push(get_blank_line(fill_attr))
i -= 1
# Clears the buffer to it's initial state, discarding all previous data.
func clear() -> void:
ydisp = 0
ybase = 0
y = 0
x = 0
lines = CircularList.new(_get_correct_buffer_length(_rows))
scroll_top = 0
scroll_bottom = _rows - 1
setup_tab_stops()
func get_wrapped_range_for_line(y: int) -> Dictionary:
var first = y
var last = y
# Scan upwards for wrapped lines
while first > 0 and lines.get_el(first).is_wrapped:
first -= 1
# Scan downwards for wrapped lines
while last + 1 < lines.length and lines.get_el(last + 1).is_wrapped:
last += 1
return {"first": first, "last": last}
# Setup the tab stops.
# @param i The index to start setting up tab stops from.
func setup_tab_stops(i = null) -> void:
if i != null:
if not tabs.get(i):
i = prev_stop(i)
else:
tabs = {}
i = 0
while i < _cols:
tabs[i] = true
i += max(_options_service.options.tab_stop_width, 1)
# Move the cursor to the previous tab stop from the given position (default is current).
# @param x The position to move the cursor to the previous tab stop.
func prev_stop(x: int) -> int:
if x == null:
x = self.x
while not tabs.get(x - 1, false) and x - 1 > 0:
x - 1
return _cols - 1 if x > _cols else 0 if x < 0 else x
# Move the cursor one tab stop forward from the given position (default is current).
# @param x The position to move the cursor one tab stop forward.
func next_stop(x = null) -> int:
if x == null:
x = self.x
x += 1
while not tabs.get(x) and x < _cols:
x += 1
return _cols - 1 if x >= _cols else 0 if x < 0 else x