- main: when the environment's locale is not UTF-8 (LANG unset, C/POSIX), switch LC_CTYPE to a UTF-8 locale so ncurses delivers and draws multibyte characters instead of dropping them. - Buffer::Id(): a process-unique id that survives moves and is fresh for copies. The terminal renderer's skip and cursor caches and the reload confirmation now key on it rather than the buffer's address, which is reused after a buffer is closed. - The reload confirmation is cancelled by any other command, like the quit confirmation. - TerminalFrontend redraws when a deferred open starts a prompt or sets a status (e.g. the swap recovery question), not only when a buffer opens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
758 lines
16 KiB
C++
758 lines
16 KiB
C++
/*
|
|
* Buffer.h - editor buffer representing an open document
|
|
*
|
|
* Buffer is the central document model in kte. Each Buffer represents one open file
|
|
* or scratch document and manages:
|
|
*
|
|
* - Content storage: Uses PieceTable for efficient text operations
|
|
* - Cursor state: Current position (curx_, cury_), rendered column (rx_)
|
|
* - Viewport: Scroll offsets (rowoffs_, coloffs_) for display
|
|
* - File backing: Optional association with a file on disk
|
|
* - Undo/Redo: Integrated UndoSystem for operation history
|
|
* - Syntax highlighting: Optional HighlighterEngine for language-aware coloring
|
|
* - Swap/crash recovery: Integration with SwapRecorder for journaling
|
|
* - Dirty tracking: Modification state for save prompts
|
|
*
|
|
* Key concepts:
|
|
*
|
|
* 1. Cursor coordinates:
|
|
* - (curx_, cury_): Logical character position in the document
|
|
* - rx_: Rendered column accounting for tab expansion
|
|
*
|
|
* 2. File backing:
|
|
* - Buffers can be file-backed (associated with a path) or scratch (unnamed)
|
|
* - File identity tracking detects external modifications
|
|
*
|
|
* 3. Legacy Line wrapper:
|
|
* - Buffer::Line provides a string-like interface for legacy command code
|
|
* - New code should prefer direct PieceTable operations
|
|
* - See DEVELOPER_GUIDE.md for migration guidance
|
|
*
|
|
* 4. Content access:
|
|
* - Rows(): Materialized line cache (legacy, being phased out)
|
|
* - GetLineView(): Zero-copy line access via string_view (preferred)
|
|
* - Direct PieceTable access for new editing operations
|
|
*/
|
|
#pragma once
|
|
#include <atomic>
|
|
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <filesystem>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
#include <string_view>
|
|
|
|
#include "PieceTable.h"
|
|
#include "UndoSystem.h"
|
|
#include <cstdint>
|
|
#include "syntax/HighlighterEngine.h"
|
|
#include "Highlight.h"
|
|
#include <mutex>
|
|
|
|
// Edit mode determines which font class is used for a buffer.
|
|
enum class EditMode { Code, Writing };
|
|
|
|
// Detect edit mode from a filename's extension.
|
|
inline EditMode
|
|
DetectEditMode(const std::string &filename)
|
|
{
|
|
std::string ext = std::filesystem::path(filename).extension().string();
|
|
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) {
|
|
return static_cast<char>(std::tolower(c));
|
|
});
|
|
static const std::unordered_set<std::string> writing_exts = {
|
|
".txt", ".md", ".markdown", ".rst", ".org",
|
|
".tex", ".adoc", ".asciidoc",
|
|
};
|
|
if (writing_exts.count(ext))
|
|
return EditMode::Writing;
|
|
return EditMode::Code;
|
|
}
|
|
|
|
// Forward declaration for swap journal integration
|
|
namespace kte {
|
|
class SwapRecorder;
|
|
}
|
|
|
|
|
|
class Buffer {
|
|
public:
|
|
Buffer();
|
|
|
|
Buffer(const Buffer &other);
|
|
|
|
Buffer &operator=(const Buffer &other);
|
|
|
|
Buffer(Buffer &&other) noexcept;
|
|
|
|
Buffer &operator=(Buffer &&other) noexcept;
|
|
|
|
explicit Buffer(const std::string &path);
|
|
|
|
// File operations
|
|
bool OpenFromFile(const std::string &path, std::string &err);
|
|
|
|
bool Save(std::string &err) const; // saves to existing filename; returns false if not file-backed
|
|
bool SaveAs(const std::string &path, std::string &err); // saves to path and makes buffer file-backed
|
|
|
|
// External modification detection.
|
|
// Returns true if the file on disk differs from the last observed identity recorded
|
|
// on open/save.
|
|
[[nodiscard]] bool ExternallyModifiedOnDisk() const;
|
|
|
|
// Refresh the stored on-disk identity to match current stat (used after open/save).
|
|
void RefreshOnDiskIdentity();
|
|
|
|
// Accessors
|
|
[[nodiscard]] std::size_t Curx() const
|
|
{
|
|
return curx_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t Cury() const
|
|
{
|
|
return cury_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t Rx() const
|
|
{
|
|
return rx_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t Nrows() const
|
|
{
|
|
return content_LineCount_();
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t Rowoffs() const
|
|
{
|
|
return rowoffs_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t Coloffs() const
|
|
{
|
|
return coloffs_;
|
|
}
|
|
|
|
|
|
// Line wrapper used by legacy command paths.
|
|
// Keep this lightweight: store materialized bytes only for that line.
|
|
class Line {
|
|
public:
|
|
Line() = default;
|
|
|
|
|
|
explicit Line(const char *s)
|
|
{
|
|
assign_from(s ? std::string(s) : std::string());
|
|
}
|
|
|
|
|
|
explicit Line(const std::string &s)
|
|
{
|
|
assign_from(s);
|
|
}
|
|
|
|
|
|
Line(const Line &other) = default;
|
|
|
|
Line &operator=(const Line &other) = default;
|
|
|
|
Line(Line &&other) noexcept = default;
|
|
|
|
Line &operator=(Line &&other) noexcept = default;
|
|
|
|
// capacity helpers
|
|
void Clear()
|
|
{
|
|
s_.clear();
|
|
}
|
|
|
|
|
|
// size/access
|
|
[[nodiscard]] std::size_t size() const
|
|
{
|
|
return s_.size();
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool empty() const
|
|
{
|
|
return s_.empty();
|
|
}
|
|
|
|
|
|
// read-only raw view
|
|
[[nodiscard]] const char *Data() const
|
|
{
|
|
return s_.data();
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t Size() const
|
|
{
|
|
return s_.size();
|
|
}
|
|
|
|
|
|
// element access (read-only)
|
|
[[nodiscard]] char operator[](std::size_t i) const
|
|
{
|
|
return (i < s_.size()) ? s_[i] : '\0';
|
|
}
|
|
|
|
|
|
// conversions
|
|
explicit operator std::string() const
|
|
{
|
|
return s_;
|
|
}
|
|
|
|
|
|
// string-like API used by command/renderer layers (implemented via materialization for now)
|
|
[[nodiscard]] std::string substr(std::size_t pos) const
|
|
{
|
|
return pos < s_.size() ? s_.substr(pos) : std::string();
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::string substr(std::size_t pos, std::size_t len) const
|
|
{
|
|
return pos < s_.size() ? s_.substr(pos, len) : std::string();
|
|
}
|
|
|
|
|
|
// minimal find() to support search within a line
|
|
[[nodiscard]] std::size_t find(const std::string &needle, const std::size_t pos = 0) const
|
|
{
|
|
return s_.find(needle, pos);
|
|
}
|
|
|
|
|
|
void erase(std::size_t pos)
|
|
{
|
|
if (pos < s_.size())
|
|
s_.erase(pos);
|
|
}
|
|
|
|
|
|
void erase(std::size_t pos, std::size_t len)
|
|
{
|
|
if (pos < s_.size())
|
|
s_.erase(pos, len);
|
|
}
|
|
|
|
|
|
void insert(std::size_t pos, const std::string &seg)
|
|
{
|
|
if (pos > s_.size())
|
|
pos = s_.size();
|
|
s_.insert(pos, seg);
|
|
}
|
|
|
|
|
|
Line &operator+=(const Line &other)
|
|
{
|
|
s_ += other.s_;
|
|
return *this;
|
|
}
|
|
|
|
|
|
Line &operator+=(const std::string &s)
|
|
{
|
|
s_ += s;
|
|
return *this;
|
|
}
|
|
|
|
|
|
Line &operator=(const std::string &s)
|
|
{
|
|
assign_from(s);
|
|
return *this;
|
|
}
|
|
|
|
private:
|
|
void assign_from(const std::string &s)
|
|
{
|
|
s_ = s;
|
|
}
|
|
|
|
|
|
std::string s_;
|
|
};
|
|
|
|
|
|
[[nodiscard]] const std::vector<Line> &Rows() const
|
|
{
|
|
ensure_rows_cache();
|
|
return rows_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::vector<Line> &Rows()
|
|
{
|
|
ensure_rows_cache();
|
|
return rows_;
|
|
}
|
|
|
|
|
|
// Lightweight, lazy per-line accessors that avoid materializing all rows.
|
|
// Prefer these over Rows() in hot paths to reduce memory overhead on large files.
|
|
[[nodiscard]] std::string GetLineString(std::size_t row) const
|
|
{
|
|
return content_.GetLine(row);
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::pair<std::size_t, std::size_t> GetLineRange(std::size_t row) const
|
|
{
|
|
return content_.GetLineRange(row);
|
|
}
|
|
|
|
|
|
// Zero-copy view of a line. Points into the materialized backing store; becomes
|
|
// invalid after subsequent edits. Use immediately.
|
|
[[nodiscard]] std::string_view GetLineView(std::size_t row) const;
|
|
|
|
|
|
[[nodiscard]] const std::string &Filename() const
|
|
{
|
|
return filename_;
|
|
}
|
|
|
|
|
|
// Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+"
|
|
// This does not mark the buffer as file-backed.
|
|
void SetVirtualName(const std::string &name)
|
|
{
|
|
filename_ = name;
|
|
is_file_backed_ = false;
|
|
is_virtual_ = true;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool IsFileBacked() const
|
|
{
|
|
return is_file_backed_;
|
|
}
|
|
|
|
|
|
// A virtual buffer (e.g. "+HELP+") has a display name that is not a path:
|
|
// it is never journaled, and saving it asks for a file name.
|
|
[[nodiscard]] bool IsVirtual() const
|
|
{
|
|
return is_virtual_;
|
|
}
|
|
|
|
|
|
// Identity of this document, stable for its lifetime, including when the
|
|
// Buffer object is moved (e.g. by the editor's buffer vector). Copies get
|
|
// a new id. Use it, not the object's address, to key per-buffer caches.
|
|
[[nodiscard]] std::uint64_t Id() const
|
|
{
|
|
return id_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool Dirty() const
|
|
{
|
|
return dirty_;
|
|
}
|
|
|
|
|
|
// Read-only flag
|
|
[[nodiscard]] bool IsReadOnly() const
|
|
{
|
|
return read_only_;
|
|
}
|
|
|
|
|
|
void SetReadOnly(bool ro)
|
|
{
|
|
read_only_ = ro;
|
|
}
|
|
|
|
|
|
void ToggleReadOnly()
|
|
{
|
|
read_only_ = !read_only_;
|
|
}
|
|
|
|
|
|
void SetCursor(const std::size_t x, const std::size_t y)
|
|
{
|
|
curx_ = x;
|
|
cury_ = y;
|
|
}
|
|
|
|
|
|
void SetRenderX(const std::size_t rx)
|
|
{
|
|
rx_ = rx;
|
|
}
|
|
|
|
|
|
void SetOffsets(const std::size_t row, const std::size_t col)
|
|
{
|
|
rowoffs_ = row;
|
|
coloffs_ = col;
|
|
}
|
|
|
|
|
|
void SetDirty(bool d)
|
|
{
|
|
// The version tracks content, not the dirty flag: every text mutation
|
|
// bumps it (see edited_at_), including undo/redo back to the saved state.
|
|
dirty_ = d;
|
|
}
|
|
|
|
|
|
// Whole-buffer content change (load, reload, replace): bump the version
|
|
// and drop all cached highlighting.
|
|
void MarkContentChanged()
|
|
{
|
|
edited_at_(0);
|
|
}
|
|
|
|
|
|
// Mark support
|
|
void ClearMark()
|
|
{
|
|
mark_set_ = false;
|
|
}
|
|
|
|
|
|
void SetMark(const std::size_t x, const std::size_t y)
|
|
{
|
|
mark_set_ = true;
|
|
mark_curx_ = x;
|
|
mark_cury_ = y;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool MarkSet() const
|
|
{
|
|
return mark_set_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t MarkCurx() const
|
|
{
|
|
return mark_curx_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t MarkCury() const
|
|
{
|
|
return mark_cury_;
|
|
}
|
|
|
|
|
|
// Visual-line selection support (multicursor/visual mode)
|
|
void VisualLineClear()
|
|
{
|
|
visual_line_active_ = false;
|
|
}
|
|
|
|
|
|
void VisualLineStart()
|
|
{
|
|
visual_line_active_ = true;
|
|
visual_line_anchor_y_ = cury_;
|
|
visual_line_active_y_ = cury_;
|
|
}
|
|
|
|
|
|
void VisualLineToggle()
|
|
{
|
|
if (visual_line_active_)
|
|
VisualLineClear();
|
|
else
|
|
VisualLineStart();
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool VisualLineActive() const
|
|
{
|
|
return visual_line_active_;
|
|
}
|
|
|
|
|
|
void VisualLineSetActiveY(std::size_t y)
|
|
{
|
|
visual_line_active_y_ = y;
|
|
}
|
|
|
|
|
|
// The selected rows, clamped to the buffer: they are stored as row numbers
|
|
// and go stale when lines are removed, and commands must never act on rows
|
|
// past the end (edits there were misplaced and could not be undone).
|
|
[[nodiscard]] std::size_t VisualLineStartY() const
|
|
{
|
|
const std::size_t y = visual_line_anchor_y_ < visual_line_active_y_ ? visual_line_anchor_y_
|
|
: visual_line_active_y_;
|
|
return clamp_row_(y);
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t VisualLineEndY() const
|
|
{
|
|
const std::size_t y = visual_line_anchor_y_ < visual_line_active_y_ ? visual_line_active_y_
|
|
: visual_line_anchor_y_;
|
|
return clamp_row_(y);
|
|
}
|
|
|
|
|
|
// In visual-line (multi-cursor) mode, the UI should highlight only the per-line
|
|
// cursor "spot" (Curx clamped to each line length), not the entire line.
|
|
[[nodiscard]] bool VisualLineSpotSelected(std::size_t y, std::size_t sx) const
|
|
{
|
|
if (!visual_line_active_)
|
|
return false;
|
|
if (y < VisualLineStartY() || y > VisualLineEndY())
|
|
return false;
|
|
std::string_view ln = GetLineView(y);
|
|
// `GetLineView()` returns the raw range, which may include a trailing '\n'.
|
|
if (!ln.empty() && ln.back() == '\n')
|
|
ln.remove_suffix(1);
|
|
const std::size_t spot = std::min(Curx(), ln.size());
|
|
return sx == spot;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::string AsString() const;
|
|
|
|
private:
|
|
[[nodiscard]] std::size_t clamp_row_(std::size_t y) const
|
|
{
|
|
const std::size_t n = Nrows();
|
|
return (n == 0) ? 0 : (y < n ? y : n - 1);
|
|
}
|
|
|
|
public:
|
|
|
|
// Syntax highlighting integration (per-buffer)
|
|
[[nodiscard]] std::uint64_t Version() const
|
|
{
|
|
return version_;
|
|
}
|
|
|
|
|
|
// Edit mode (code vs writing)
|
|
[[nodiscard]] EditMode GetEditMode() const
|
|
{
|
|
return edit_mode_;
|
|
}
|
|
|
|
|
|
void SetEditMode(EditMode m)
|
|
{
|
|
edit_mode_ = m;
|
|
edit_mode_detected_ = true;
|
|
}
|
|
|
|
|
|
void ToggleEditMode()
|
|
{
|
|
edit_mode_ = (edit_mode_ == EditMode::Code)
|
|
? EditMode::Writing
|
|
: EditMode::Code;
|
|
edit_mode_detected_ = true;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool EditModeDetected() const
|
|
{
|
|
return edit_mode_detected_;
|
|
}
|
|
|
|
|
|
void SetSyntaxEnabled(bool on)
|
|
{
|
|
syntax_enabled_ = on;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool SyntaxEnabled() const
|
|
{
|
|
return syntax_enabled_;
|
|
}
|
|
|
|
|
|
// Marks that the user explicitly set syntax state via a command (:syntax
|
|
// on/off, :set filetype=...), as opposed to it being auto-applied from
|
|
// GUIConfig. Frontends that re-apply the config-driven default every
|
|
// frame (e.g. ImGuiFrontend::apply_syntax_to_buffer) must check this
|
|
// first, mirroring EditModeDetected()'s "don't stomp a manual toggle"
|
|
// pattern - otherwise a manual :syntax off is silently undone on the very
|
|
// next frame.
|
|
void SetSyntaxUserOverride(bool on)
|
|
{
|
|
syntax_user_override_ = on;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool SyntaxUserOverride() const
|
|
{
|
|
return syntax_user_override_;
|
|
}
|
|
|
|
|
|
void SetFiletype(const std::string &ft)
|
|
{
|
|
filetype_ = ft;
|
|
}
|
|
|
|
|
|
[[nodiscard]] const std::string &Filetype() const
|
|
{
|
|
return filetype_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] kte::HighlighterEngine *Highlighter()
|
|
{
|
|
return highlighter_.get();
|
|
}
|
|
|
|
|
|
[[nodiscard]] const kte::HighlighterEngine *Highlighter() const
|
|
{
|
|
return highlighter_.get();
|
|
}
|
|
|
|
|
|
void EnsureHighlighter()
|
|
{
|
|
if (!highlighter_)
|
|
highlighter_ = std::make_unique<kte::HighlighterEngine>();
|
|
}
|
|
|
|
|
|
// Swap journal integration (set by Editor)
|
|
void SetSwapRecorder(kte::SwapRecorder *rec)
|
|
{
|
|
swap_rec_ = rec;
|
|
}
|
|
|
|
|
|
[[nodiscard]] kte::SwapRecorder *SwapRecorder() const
|
|
{
|
|
return swap_rec_;
|
|
}
|
|
|
|
|
|
// Raw, low-level editing APIs used by UndoSystem apply().
|
|
// These must NOT trigger undo recording. They also do not move the cursor.
|
|
void insert_text(int row, int col, std::string_view text);
|
|
|
|
void delete_text(int row, int col, std::size_t len);
|
|
|
|
void split_line(int row, int col);
|
|
|
|
void join_lines(int row);
|
|
|
|
void insert_row(int row, std::string_view text);
|
|
|
|
void delete_row(int row);
|
|
|
|
// Replace the entire buffer content with raw bytes.
|
|
// Intended for crash recovery (swap replay) and test harnesses.
|
|
// This does not trigger swap or undo recording.
|
|
void replace_all_bytes(std::string_view bytes);
|
|
|
|
// Undo system accessors (created per-buffer)
|
|
[[nodiscard]] UndoSystem *Undo();
|
|
|
|
[[nodiscard]] const UndoSystem *Undo() const;
|
|
|
|
#if defined(KTE_TESTS)
|
|
// Test-only: return the raw buffer bytes (including newlines) as a string.
|
|
[[nodiscard]] std::string BytesForTests() const;
|
|
#endif
|
|
|
|
private:
|
|
struct FileIdentity {
|
|
bool valid = false;
|
|
std::uint64_t mtime_ns = 0;
|
|
std::uint64_t size = 0;
|
|
std::uint64_t dev = 0;
|
|
std::uint64_t ino = 0;
|
|
};
|
|
|
|
[[nodiscard]] static bool stat_identity(const std::string &path, FileIdentity &out);
|
|
|
|
[[nodiscard]] bool current_disk_identity(FileIdentity &out) const;
|
|
|
|
mutable FileIdentity on_disk_identity_{};
|
|
|
|
// State mirroring original C struct (without undo_tree)
|
|
std::size_t curx_ = 0, cury_ = 0; // cursor position in characters
|
|
std::size_t rx_ = 0; // render x (tabs expanded)
|
|
std::size_t nrows_ = 0; // number of rows
|
|
std::size_t rowoffs_ = 0, coloffs_ = 0; // viewport offsets
|
|
mutable std::vector<Line> rows_; // materialized cache of rows (without trailing newlines)
|
|
// PieceTable is the source of truth.
|
|
PieceTable content_{};
|
|
mutable bool rows_cache_dirty_ = true; // invalidate on edits / I/O
|
|
|
|
// Helper to rebuild rows_ from content_
|
|
void ensure_rows_cache() const;
|
|
|
|
// Helper to query content_.LineCount() while keeping header minimal
|
|
std::size_t content_LineCount_() const;
|
|
|
|
std::string filename_;
|
|
bool is_file_backed_ = false;
|
|
bool is_virtual_ = false;
|
|
std::uint64_t id_ = NextBufferId();
|
|
|
|
static std::uint64_t NextBufferId()
|
|
{
|
|
static std::atomic<std::uint64_t> next{1};
|
|
return next.fetch_add(1);
|
|
}
|
|
bool dirty_ = false;
|
|
bool read_only_ = false;
|
|
bool mark_set_ = false;
|
|
std::size_t mark_curx_ = 0, mark_cury_ = 0;
|
|
bool visual_line_active_ = false;
|
|
std::size_t visual_line_anchor_y_ = 0;
|
|
std::size_t visual_line_active_y_ = 0;
|
|
|
|
// Per-buffer undo state
|
|
std::unique_ptr<struct UndoTree> undo_tree_;
|
|
std::unique_ptr<UndoSystem> undo_sys_;
|
|
|
|
// Edit mode (code vs writing)
|
|
EditMode edit_mode_ = EditMode::Code;
|
|
bool edit_mode_detected_ = false; // true after initial auto-detection
|
|
|
|
// Syntax/highlighting state
|
|
std::uint64_t version_ = 0; // increment on edits
|
|
|
|
// Record a content change starting at `row`: bump the version and let the
|
|
// highlighter drop cached rows from `row` down (rows above are unaffected).
|
|
void edited_at_(int row)
|
|
{
|
|
++version_;
|
|
if (highlighter_)
|
|
highlighter_->OnEdit(row < 0 ? 0 : row, version_);
|
|
}
|
|
bool syntax_enabled_ = true;
|
|
bool syntax_user_override_ = false; // true once user explicitly set syntax state via a command
|
|
std::string filetype_;
|
|
std::unique_ptr<kte::HighlighterEngine> highlighter_;
|
|
// Non-owning pointer to swap recorder managed by Editor/SwapManager
|
|
kte::SwapRecorder *swap_rec_ = nullptr;
|
|
|
|
mutable std::mutex buffer_mutex_;
|
|
}; |