mirror of
https://github.com/lihop/godot-xterm.git
synced 2024-11-14 14:30:24 +01:00
430 lines
14 KiB
GDScript
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
|