Cleanup and minor Fixes

This commit is contained in:
2026-01-28 18:46:44 +00:00
parent cca0177929
commit 6abdefa808
22 changed files with 343 additions and 371 deletions

2
.gitignore vendored
View File

@@ -13,5 +13,7 @@ build
bin
.thinlto-cache/
Gemfile*
.ruby-lsp/
__old__

View File

@@ -13,14 +13,14 @@ CCACHE := ccache
CXX := $(CCACHE) clang++
CFLAGS_DEBUG :=\
-std=c++20 -Wall -Wextra \
-std=c++23 -Wall -Wextra -Wno-c23-extensions \
-O0 -fno-inline -gsplit-dwarf\
-g -fno-omit-frame-pointer\
-Wno-unused-command-line-argument \
-I./include -I./libs \
-I/usr/include/ruby-3.4.0 -I/usr/include/ruby-3.4.0/x86_64-linux
CFLAGS_RELEASE :=\
-std=c++20 -O3 -march=native \
-std=c++23 -O3 -march=native \
-fno-rtti -fstrict-aliasing \
-ffast-math -flto=thin \
-fvisibility=hidden -fuse-ld=lld \
@@ -28,6 +28,7 @@ CFLAGS_RELEASE :=\
-mllvm -vectorize-loops \
-fno-unwind-tables -fno-asynchronous-unwind-tables\
-Wno-unused-command-line-argument \
-Wno-c23-extensions \
-I./include -I./libs \
-I/usr/include/ruby-3.4.0 -I/usr/include/ruby-3.4.0/x86_64-linux
@@ -41,7 +42,7 @@ UNICODE_OBJ_RELEASE := $(patsubst libs/unicode_width/%.c,$(OBJ_DIR)/release/unic
LIBS := \
libs/libgrapheme/libgrapheme.a \
-lpcre2-8 -lmagic -lruby
-Wl,-Bstatic -lpcre2-8 -Wl,-Bdynamic -lmagic -lruby
SRC := $(wildcard $(SRC_DIR)/**/*.cc) $(wildcard $(SRC_DIR)/*.cc)
OBJ_DEBUG := $(patsubst $(SRC_DIR)/%.cc,$(OBJ_DIR)/debug/%.o,$(SRC))

150
README.md
View File

@@ -51,16 +51,19 @@ And any modern terminal should work fine - preferably `kitty` or `wezterm`.<br>
#### `./libs` folder
Some other dependancies like `libgrapheme` and `unicode_width` are added as submodules or copied.<br>
`unicode_width` is compiled by the makefile so nothing to do there.<br>
`libgrapheme` needs to be compiled using `make` in it's folder.<br>
Some other dependancies are added as submodules or copied.<br>
- `unicode_width` is compiled by the makefile so nothing to do there.<br>
- `libgrapheme` needs to be compiled using `make` in it's folder.<br>
- ``
#### LSPs
The following lsp's are supported and can be installed anywhere in your `$PATH`<br>
Lsp's are defined in the `libcrib.rb` file and you can use your `config/main.rb` file to add more.<br>
The following lsp's are added by default and can be installed anywhere in your `$PATH`<br>
* [clangd](https://clangd.llvm.org/)
* [solargraph](https://solargraph.org/)
* [ruby-lsp](https://shopify.github.io/ruby-lsp/)
* [bash-language-server](https://github.com/bash-lsp/bash-language-server)
* [vscode-css-language-server](https://github.com/hrsh7th/vscode-langservers-extracted)
* [vscode-json-language-server](https://github.com/hrsh7th/vscode-langservers-extracted)
@@ -81,14 +84,11 @@ The following lsp's are supported and can be installed anywhere in your `$PATH`<
* [make-language-server](https://github.com/Freed-Wu/autotools-language-server)
> As it is still in development, some of these may not work as expected or that well.<br>
> But for c/ruby/lua/python it should work fine (I test more with these).<br>
> It should work even if the lsp is not installed but lsp features will not work.<br>
> See `include/config.h` & `include/ts/decl.h` if you want to add your own lsp and/or tree-sitter grammar.<br>
#### Compiler
`g++` and `clang++` should both work fine but `c++20+` is required.
The makefile uses `clang++` by default.<br>
`clang++` should work fine but `c++23+` is required.<br>
Can remove `ccache` if you want from the makefile.<br>
#### Compliling
@@ -100,9 +100,6 @@ make release
### Running
Preferably add the `bin` folder to PATH or move `bin/crib` to somewhere in PATH.<br>
But make sure that `scripts/` are at `../` relative to the binary or it will crash.<br>
`scripts/init.sh` and `scripts/exit.sh` can be used to add hooks to the editor on startup and exit
(Make sure to remove my `kitty` hooks from them if you want).<br>
For some LSP's to work properly `crib` needs to be run from the root folder of the project. *To be fixed*<br>
then do -<br>
@@ -110,127 +107,12 @@ then do -<br>
crib ./filename.ext
```
*If `filename.ext` does not exist, it will fail to load the editor - use `touch filename.ext` to create it - to be fixed*<br>
*Try out with files in `samples/`*<br>
*If `filename.ext` does not exist, it will be created*<br>
## Keybindings
### Mouse Interactions
These interactions work globally or generally across the editor canvas.
| Action | Function |
| --- | --- |
| **Scroll Up/Down** | Scrolls the view. |
| **Scroll Left/Right** | Moves the cursor left or right. |
| **Left Click (Press)** | Moves cursor to position; resets selection. |
| **Left Click (Double)** | Selects the **word** under the cursor (enters SELECT mode). |
| **Left Click (Triple)** | Selects the **line** under the cursor (enters SELECT mode). |
| **Left Click (Drag)** | Selects text (Character, Word, or Line based on initial click type). |
| **Left Click (Release)** | If cursor and selection start are the same, returns to NORMAL mode. |
### Navigation (Global / Special Keys)
These keys work primarily in Normal mode but handle movement logic.
| Key | Modifier | Function |
| --- | --- | --- |
| **Arrows** (Up/Down/Left/Right) | None | Move cursor 1 step in that direction. |
| **Arrows** (Up/Down) | `CTRL` | Move cursor **5 steps** in that direction. |
| **Arrows** (Left/Right) | `CTRL` | Jump to the previous/next **word boundary**. |
| **Arrows** (Up/Down) | `ALT` | **Move the current line** Up or Down. |
| **Arrows** (Left/Right) | `ALT` | Move cursor **8 steps** in that direction. |
### NORMAL Mode
This is the default navigation and command mode.
| Key | Function |
| --- | --- |
| **i** | Enter **INSERT** mode (at current position). |
| **a** | Enter **INSERT** mode (append: moves cursor right by 1 first). |
| **s** or **v** | Enter **SELECT** mode (start character selection). |
| **:** or **;** | Enter **RUNNER** mode (Command Bar). |
| **u** | Select the **last line** of the file (Jumps to bottom). |
| **h** | Trigger **LSP Hover** information for the symbol under cursor. |
| **Ctrl + h** | Scroll the hover window **Up**. |
| **Ctrl + l** | Scroll the hover window **Down**. |
| **Ctrl + s** | **Save** the file. |
| **Ctrl + d** | Scroll Page **Down** (1 unit). |
| **Ctrl + u** | Scroll Page **Up** (1 unit). |
| **p** | **Paste** from clipboard at cursor position (moves cursor to end of paste). |
| **>** or **.** | **Indent** the current line. |
| **<** or **,** | **Dedent** (un-indent) the current line. |
| **Space** | Move cursor Right. |
| **Backspace** (`0x7F`) | Move cursor Left. |
| **Enter** (`\n`, `\r`) | Move cursor Down. |
| **\| or \\** | Move cursor Up. |
| **n** | Enter **JUMPER** mode (Set Bookmark). |
| **m** | Enter **JUMPER** mode (Jump to Bookmark). |
| **N** | Clear specific Jumper hook (logic attempts to clear hook at current line). |
### INSERT Mode
Used for typing text.
| Key | Function |
| --- | --- |
| **Esc** (`0x1B`) | Return to **NORMAL** mode. |
| **Tab** (`\t`) | Inserts 2 spaces. |
| **Enter** | Inserts newline + **Auto-indents** based on previous line/context. |
| **Backspace** | Deletes previous character or auto-collapses empty pairs (e.g., `{` -> `}`). |
| **Ctrl + w** | **Delete Previous Word**. |
| **Del** | Delete character under cursor. |
| **Ctrl + Del** | Delete **Next Word**. |
| **Typing** | Inserts characters. |
| **Ctrl + Shift + v or as configured in your terminal** | System pasting. |
| **{ ( [ " '** | Auto-inserts closing pair (e.g., typing `{` inserts `{}`). |
| **} ) ] " '** | If the next char matches the typed char, skip insertion (overwrite), otherwise insert. |
#### Autocompletion (Inside Insert Mode)
These function only if LSP and completion are active.
| Key | Function |
| --- | --- |
| **Ctrl + p** | Select **Next** completion item. |
| **Ctrl + o** | Select **Previous** completion item. |
| **Ctrl + \\** | **Accept** selected completion OR trigger new completion request. |
| **Trigger Chars** | (e.g., `.`, `>`) Automatically triggers completion popup. |
### SELECT Mode
Used for highlighting text.
| Key | Function |
| --- | --- |
| **Esc**, **s**, **v** | Cancel selection and return to **NORMAL** mode. |
| **y** | **Yank (Copy)** selection to clipboard → Return to Normal. |
| **x** | **Cut** selection to clipboard → Return to Normal. |
| **p** | **Paste** over selection (Replace text) → Return to Normal. |
| **f** | **Fold** the selected range (collapses code) → Return to Normal. |
### JUMPER Mode
This mode uses a bookmarking system mapped to keyboard characters.
* **Entered via `n` (Set Mode):**
* Pressing any key `!` through `~` assigns the current line number to that key.
* **Entered via `m` (Jump Mode):**
* Pressing any key `!` through `~` jumps the cursor to the line previously assigned to that key.
### RUNNER Mode (Command Bar)
Activated by `:` or `;`.
| Key | Function |
| --- | --- |
| **Esc** | Cancel and return to **NORMAL** mode. |
| **Enter** | Execute the typed command. |
| **Left / Right** | Move cursor within the command bar. |
| **Up / Down** | Intended for command history. (Not implemented) |
| **Typing** | Insert characters into the command bar. (Not implemented) |
TODO: add keybind information on how to set in `config/main.rb`
and default / unchangeable keybinds
## Features Implemented
@@ -250,7 +132,7 @@ Activated by `:` or `;`.
- hooks jumping (bookmarking)
- color hex code highlighting
- current line highlighting
<!-- - TODO: current word under cursor highlighting -->
- all instances of current word under cursor highlighting
#### syntax highlighting and filetype detection (using extention or libmagic) for:
- ruby
@@ -286,11 +168,13 @@ Activated by `:` or `;`.
#### LSP-powered features:
- diagnostics
- autocompletion
- hover docs (with markdown support)
- hover docs
- formatting support
- Full file formatting on save
- Ontype formatting when inserting special characters defined by the language server
- *(few lsp's actually support this - try to configure a few more which can but need configuration and for others need to add support for external formatters)*
- A list of all supported lsp's can be found [here](#lsps).
- A list of some lsp's can be found [here](#lsps).
- Any lsp can be added to the `config/main.rb` file.
- Though not all might work well. Open an issue if you find a lsp that doesn't work well.
**A lot lot more to come**

69
TODO.md
View File

@@ -2,15 +2,12 @@ Copyright 2025 Syed Daanish
# TODO
### Critical Fixes
##### Check each lsp with each of the features implemented
##### BTW Check each lsp with each of the features implemented
* Static link pcre2-8 and libmagic and add precompiled libmagic magic database for better file detection
* [ ] **LSP Bug:** Check why `fish-lsp` is behaving so off with completions filtering.
* [ ] **File IO:** Normalize/validate unicode on file open (enforce UTF-8, handle other types gracefully).
* [ ] **Critical Crash:** Fix bug where closing immediately while LSP is still loading hangs and then segfaults (especially on slow ones like fish-lsp where quick edits and exit can hang).
* [ ] **Line move:** fix the move line functions to work without the calculations from folds as folds are removed.
* [ ] **Modularize handle_events and renderer functions:** The function is over 700 lines with a lot of repeating blocks. Split into smaller functions.
* [ ] **Editor Indentation Fix:** - Main : merger indentation with the parser for more accurate results.
* [ ] Keep cache of language maps in engine to reduce lookup time.
* [ ] In indents add function to support tab which indents if before any content and inserts a pure \t otherwise.
@@ -20,22 +17,35 @@ Copyright 2025 Syed Daanish
* [ ] These will dedent when the block immediately after them is dedented
* [ ] Dont dedent if ending is valid starting is invalid but also empty
* [ ] Just leave asis if starting is empty
* [ ] **Readme:** Update readme to show indetation mechanics.
* [ ] **LSP Bug:** Try to find out why emojis are breaking lsp edits. (check the ruby sample)
* [ ] **Readme:** Update readme to show ruby based config.
* [ ] **UI Refinement:**
* [ ] Allow completion list to be scrolled; show only `x` max items.
* [ ] Finish autocomplete box style functions.
* [ ] **Documentation UI:** Capture `Ctrl+h` / `Ctrl+l` for scrolling documentation windows.
* [ ] **Redo hooks and folding as proper engines**: With functions to checkstate/cursor like function and edits application.
* [ ] Do trextmate like regex grammar parsing with lsp symbols for semantic highlighting.
* Probably remove tre--sitter or just keep it for context tree.
* Making bracket matching andignoring strings/comments easier.
* remove tree-sitter mention from everywhere especially submodules
* make it faster for line inserts/deletes too (treeify the vector)
* [ ] Redo hooks as a struct.
* [ ] breakdown the render function into smaller functions.
* Try to make all functions better now that folds have been purged
* Cleanup syntax and renderer files
probably remove solargraph support and use ruby-lsp (or sorbet?) instead
* Add a thing called view which is a rect with speacial type editor . but otherwise a ruby or c++ view
* can be used for stuff like file manager/git manager/theme picker.
* allow flushing functions in ruby to tell c++ to refresh keybinds/themes etc.
* allow keybinds to be set in ruby
check::
pull diagnostics for ruby-lsp
lsp selection range - use to highlight start / end of range maybe?
goto definiton
signature help
document symbol for the top bar maybe? (or workspace symbol)
also setup workspaces
Semantic highlighting
Quick fixes
Rename symbols
probably remove solargraph support and use ruby-lsp (or sorbet?) instead because it is a pain for utf stuff
ruby-lsp also supports erb so thats a plus
move lsp configs to json and also allow configs for windows-style vs unix-style line endings and utf-8 vs utf-16
@@ -55,7 +65,7 @@ move lsp configs to json and also allow configs for windows-style vs unix-style
* [ ] **Tree-sitter Indent:** Attempt to allow Tree-sitter to handle indentation if possible.
* [ ] **Scrolling:** Add logic where selecting at the end of the screen scrolls down (and vice versa).
* *Implementation:* Update `main.cc` to send drag events to the selected editor.
* *Implementation:* Update `main.cc` to send drag events to the selected editor even if coordinates are out of bounds.
### UX
@@ -72,11 +82,6 @@ move lsp configs to json and also allow configs for windows-style vs unix-style
* [ ] Handle snippets properly in autocomplete: use only the last word in signature when replacing and set cursor to the first one.
* [ ] **Basic Autocomplete:** Keep a list of words in the current buffer for non-LSP fallback.
* [ ] **Language Support:**
* [ ] Add ECMA to JS and make TSX support.
* [ ] Add formatting for files where LSP doesn't provide it.
* [ ] Redo grammar files properly (especially cpp).
### Major Features
@@ -93,21 +98,12 @@ move lsp configs to json and also allow configs for windows-style vs unix-style
* [ ] **Block Selection:**
* [ ] Double-clicking a bracket selects the whole block (first time only) and sets mode to `WORD`.
* [ ] **Tree-sitter Context:**
* [ ] Get code context from Tree-sitter.
* [ ] Get node path of current cursor and add indicator bar (breadcrumbs).
* [ ] Highlight block edges when cursor is on/in a bracket.
### Visuals, UI & Extensions?
*Focus: Aesthetics and external integrations.*
* [ ] **Status Bar:** Complete status bar and command runner.
* [ ] **Visual Aids:**
* [ ] Expand color regex to match CSS colors in CSS files.
* [ ] Add color picker/palette.
* [ ] Add color picker/palette.
* [ ] **Git:** Add Git integration (status, diffs).
* [ ] **AI/Snippets:**
@@ -120,21 +116,8 @@ move lsp configs to json and also allow configs for windows-style vs unix-style
### Optimizations & Fluff
* [ ] **Event Loop:**
* [ ] Make the whole engine event-driven rather than clock-driven.
* [ ] Mybe keep background thread with dirty flag.
* [ ] But merge input and render into a single loop that only renders when input affects render or background thread needs refresh and try to couple multiple renders.
* [ ] LSP and inputs should be blocking (lsp on its fd) and inputs in io/input.cc
* [ ] **Performance:**
* [ ] Switch JSON parser to `RapidJSON` (or similar high-performance lib).
* [ ] Decrease usage of `std::string` in UI, LSP, and warnings.
* [ ] Also for vectors into managed memory especially for completions/lsp-stuff.
* [ ] **Folding:** Redo folding system and its relation to `move_line_*` functions.
* [ ] **Grammars:**
* [ ] Manually add redo SCM files (especially cpp/c/h).
* [ ] Create `lua-typed` and `man pages` Tree-sitter grammars.
* [ ] **Repo Maintenance:** Once renderer is proven check commit `43f443e`.

View File

@@ -1,6 +1,6 @@
require_relative "libcrib"
# basic configuration
# This can also be used to do speacail configs for different projects.
# its ruby guys script whatever you want.
C.startup do
puts "Starting crib..."
@@ -86,7 +86,7 @@ C.theme = {
# lsp: "solargraph"
# }
C.line_endings = :unix # or :windows
C.line_endings = :auto_unix # or :unix or :windows or :auto_windows
# C.extra_highlights do |_line, _idx|
# # the return can be an array of
@@ -99,7 +99,7 @@ C.line_endings = :unix # or :windows
# The highlighter will be aplied to the language as long as the langauge is defined in C.languages
C.highlighters[:ruby_n] = {
parser: ->(line, state) {
parser: ->(line, state, line_idx) {
# the return value is a hash
# it contains the state and the highlights
# state can be of any type but will be consistent between calls
@@ -108,14 +108,14 @@ C.highlighters[:ruby_n] = {
# the highlights can be an array of
# [K_type, start, end]
# K_type is a constant from the constants defined in libcrib.rb
# for ex: for strings it would be C::K_STRING or for numbers C::K_NUMBER etc.
# for ex: for strings it would be Tokens::K_STRING or for numbers Tokens::K_NUMBER etc.
# and start and end are integers strictly inside the line
return {
state: "",
tokens: [
# This will highlight the entire line as a string
# Any wrong format will not be handled and lead to crashes
{ type: C::K_STRING, start: 0, end: line.length }
{ type: Tokens::K_STRING, start: 0, end: line.length }
]
}
},

View File

@@ -11,6 +11,7 @@
#include "ui/diagnostics.h"
#include "ui/hover.h"
#include "utils/utils.h"
#include <cstdint>
#define CHAR 0
#define WORD 1
@@ -57,7 +58,7 @@ struct Editor {
};
Editor *new_editor(const char *filename_arg, Coord position, Coord size,
bool unix_eol);
uint8_t eol);
void save_file(Editor *editor);
void free_editor(Editor *editor);
void render_editor(Editor *editor);

View File

@@ -7,15 +7,16 @@
extern std::unordered_map<std::string, std::pair<VALUE, VALUE>>
custom_highlighters;
void ruby_start(const char *main_file);
void ruby_start();
void ruby_shutdown();
void ruby_log(std::string msg);
void load_theme();
void load_languages_info();
bool read_line_endings();
uint8_t read_line_endings();
void load_custom_highlighters();
VALUE parse_custom(std::vector<Token> *tokens, VALUE parser_block,
const char *line, uint32_t len, VALUE state);
const char *line, uint32_t len, VALUE state,
uint32_t line_num);
bool custom_compare(VALUE match_block, VALUE state1, VALUE state2);
#endif

View File

@@ -26,6 +26,24 @@ const std::unordered_map<std::string, TokenKind> kind_map = {
#undef ADD
};
constexpr const char tokens_def[] = "module Tokens\n"
#define STRINGIFY_HELPER(x) #x
#define STRINGIFY(x) STRINGIFY_HELPER(x)
#define ADD(name) " " #name " = " STRINGIFY(__COUNTER__) "\n"
#include "syntax/tokens.def"
#undef ADD
#undef STRINGIFY
#undef STRINGIFY_HELPER
" freeze\n"
"end";
constexpr const char crib_module[] = {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wc23-extensions"
#embed "libcrib.rb"
#pragma clang diagnostic pop
, '\0'};
extern std::array<Highlight, TOKEN_KIND_COUNT> highlights;
struct Token {

View File

@@ -4,9 +4,9 @@
#include "syntax/decl.h"
#define DEF_LANG(name) \
std::shared_ptr<void> name##_parse(std::vector<Token> *tokens, \
std::shared_ptr<void> in_state, \
const char *text, uint32_t len); \
std::shared_ptr<void> name##_parse( \
std::vector<Token> *tokens, std::shared_ptr<void> in_state, \
const char *text, uint32_t len, uint32_t line_num); \
bool name##_state_match(std::shared_ptr<void> state_1, \
std::shared_ptr<void> state_2);
@@ -34,9 +34,9 @@ DEF_LANG(bash);
inline static const std::unordered_map<
std::string,
std::tuple<std::shared_ptr<void> (*)(std::vector<Token> *tokens,
std::shared_ptr<void> in_state,
const char *text, uint32_t len),
std::tuple<std::shared_ptr<void> (*)(
std::vector<Token> *tokens, std::shared_ptr<void> in_state,
const char *text, uint32_t len, uint32_t line_num),
bool (*)(std::shared_ptr<void> state_1,
std::shared_ptr<void> state_2)>>
parsers = {

View File

@@ -1,57 +1,4 @@
module C
K_DATA = 0
K_SHEBANG = 1
K_COMMENT = 2
K_ERROR = 3
K_STRING = 4
K_ESCAPE = 5
K_INTERPOLATION = 6
K_REGEXP = 7
K_NUMBER = 8
K_TRUE = 9
K_FALSE = 10
K_CHAR = 11
K_KEYWORD = 12
K_KEYWORDOPERATOR = 13
K_OPERATOR = 14
K_FUNCTION = 15
K_TYPE = 16
K_CONSTANT = 17
K_VARIABLEINSTANCE = 18
K_VARIABLEGLOBAL = 19
K_ANNOTATION = 20
K_DIRECTIVE = 21
K_LABEL = 22
K_BRACE1 = 23
K_BRACE2 = 24
K_BRACE3 = 25
K_BRACE4 = 26
K_BRACE5 = 27
K_HEADING1 = 28
K_HEADING2 = 29
K_HEADING3 = 30
K_HEADING4 = 31
K_HEADING5 = 32
K_HEADING6 = 33
K_BLOCKQUOTE = 34
K_LIST = 35
K_LISTITEM = 36
K_CODE = 37
K_LANGUAGENAME = 38
K_LINKLABEL = 39
K_IMAGELABEL = 40
K_LINK = 41
K_TABLE = 42
K_TABLEHEADER = 43
K_ITALIC = 44
K_BOLD = 45
K_UNDERLINE = 46
K_STRIKETHROUGH = 47
K_HORIXONTALRULE = 48
K_TAG = 49
K_ATTRIBUTE = 50
K_CHECKDONE = 51
K_CHECKNOTDONE = 52
@lsp_config = {
"clangd" => [
"--background-index",
@@ -195,7 +142,7 @@ module C
extensions: ["erb"],
filenames: [],
mimetypes: ["text/x-erb"],
lsp: "emmet-language-server"
lsp: "ruby-lsp"
},
lua: {
color: 0x36a3d9,
@@ -350,11 +297,45 @@ module C
lsp: "bash-language-server"
}
}
@theme = {
:default => { fg: 0xEEEEEE },
:shebang => { fg: 0x7DCFFF },
:error => { fg: 0xEF5168 },
:comment => { fg: 0xAAAAAA, italic: true },
:string => { fg: 0xAAD94C },
:escape => { fg: 0x7DCFFF },
:interpolation => { fg: 0x7DCFFF },
:regexp => { fg: 0xD2A6FF },
:number => { fg: 0xE6C08A },
# rubocop:disable Lint/BooleanSymbol
:true => { fg: 0x7AE93C },
:false => { fg: 0xEF5168 },
# rubocop:enable Lint/BooleanSymbol
:char => { fg: 0xFFAF70 },
:keyword => { fg: 0xFF8F40 },
:keywordoperator => { fg: 0xF07178 },
:operator => { fg: 0xFFFFFF, italic: true },
:function => { fg: 0xFFAF70 },
:type => { fg: 0xF07178 },
:constant => { fg: 0x7DCFFF },
:variableinstance => { fg: 0x95E6CB },
:variableglobal => { fg: 0xF07178 },
:annotation => { fg: 0x7DCFFF },
:directive => { fg: 0xFF8F40 },
:label => { fg: 0xD2A6FF },
:brace1 => { fg: 0xD2A6FF },
:brace2 => { fg: 0xFFAFAF },
:brace3 => { fg: 0xFFFF00 },
:brace4 => { fg: 0x0FFF0F },
:brace5 => { fg: 0xFF0F0F }
}
@line_endings = :auto_unix
@key_handlers = {}
@key_binds = {}
@highlighters = {}
@log_queue = []
@line_endings = :unix
@b_startup = nil
@b_shutdown = nil
class << self
attr_accessor :theme, :lsp_config, :languages,

View File

@@ -10,7 +10,8 @@ struct Parser {
std::string lang;
std::shared_ptr<void> (*parse_func)(std::vector<Token> *tokens,
std::shared_ptr<void> in_state,
const char *text, uint32_t len);
const char *text, uint32_t len,
uint32_t line_num);
bool (*state_match_func)(std::shared_ptr<void> state_1,
std::shared_ptr<void> state_2);
VALUE parser_block = Qnil;

View File

@@ -1,4 +1,3 @@
// When updating this file with new types do not forget to update libcrib.rb to parallel changes
ADD(K_DATA)
ADD(K_SHEBANG)
ADD(K_COMMENT)

View File

@@ -154,7 +154,7 @@ std::string path_abs(const std::string &path_str);
std::string path_to_file_uri(const std::string &path_str);
std::string filename_from_path(const std::string &path);
std::string get_exe_dir();
char *load_file(const char *path, uint32_t *out_len);
char *load_file(const char *path, uint32_t *out_len, bool *out_eol);
char *detect_file_type(const char *filename);
Language language_for_file(const char *filename);

View File

@@ -47,7 +47,7 @@ while ((counter < 5)); do
done
# Subshelled loops and alternating quoting
for item in "${ITEMS[@]}"; do
for item in "${ITEMS[@]}}"; do
(
msg="Processing $item"
echo "$(colorize blue "$msg")"
@@ -127,7 +127,7 @@ grep "world" <<<"$FOO" >/dev/null && log INFO "FOO contains world"
diff <(echo foo) <(echo foo) >/dev/null && log INFO "diff matched"
# Command substitution with pipeline
timestamp=$(date | sed 's/ /_/g')
timestamp=$(date | sed 's//_/g')
log INFO "Timestamp: $timestamp"
# Testing array slicing

View File

@@ -4,13 +4,12 @@
# Purpose: Test syntax highlighting + width calculation in your editor
# ---------------------------------------------------------------
# Mixed-width CJKssssssssssssssss LoadErssssssssssssssssssssssss
cjk_samples = [
"漢字テスト",
"測試中文字串",
"한국어 테스트",
"ひらがなカタカナ混合",
'漢字テスト',
'測試中文字串',
'한국어 테스트',
'ひらがなカタカナ混合'
]
# a hex color: #FFFFFF shouldn't hl here: hsl(147rad, 50%, 47%) as it is not css-style file
@@ -18,34 +17,33 @@ cjk_samples = [
0x603010 # another hex color
# Ruby regex with unicode
$unicode_regex_multiline = /[一-龯ぁ-ん#{0x3000}
$unicode_regex_multiline = /[一-龯ぁ-ん12288
\-ヶー
s wow
々〆〤]/
UNICORE = %r{
[s]
UNICORE = /
s
{#{ss}}
\C-s\u{10}
}
/
UNINITCORE = %{
UNINITCORE = %(
{{#{}}}
test = "A:\x41 B:\101 C:\u0043 D:\u{44 45} NUL:\0 DEL:\c? CTRL_A:\cA META_X:\M-x CTRL_META_X:\C-\M-x MIX:\C-\M-z N:\N{UNICODE NAME}"
}
)
# Unicode identifiers (valid in Ruby)
= 0x5_4eddaee
π = 3.14_159e+2, ?\u0234, ?\,, ?\x0A, ?s, true, false, 0
π = 0.314_159e+2, ?\u0234, "\,", ?\x0A, 's', true, false, 0
= -> { "こんに \n ちは" }
arr = Array.new()
not_arr = NotABuiltin.new()
arr = []
not_arr = NotABuiltin.new
raise NameError or SystemExit or CustomError or Errno or ErrorNotAtAll
@@ -56,7 +54,7 @@ end
# Iterate through CJK samples
cjk_samples.each_with_index do |str, idx:|
puts %Q! CJK[#{idx}] => #{str} (len=#{str.length})\! !
puts %! CJK[#{idx}] => #{str} (len=#{str.length})\! !
symbol = :"
a
"
@@ -67,49 +65,46 @@ end
puts "Emoji count: #{emojis.length}"
# Multi-line string with unicode
multi = <<BASH
# Function recursion demo
factorial() {
local n="$1"
if ((n <= 1)); then
echo 1
else\ns
local prev
prev=$(factorial $((n - 1)))
echo $((n * prev))
before #{ interpol
# comment should be fine heres s
$a / $-s+0xFF
}s
x
a after
fi
} #{s}
log INFO "factorial(5) = $(factorial 5)"
multi = <<~BASH
# Function recursion demo
factorial() {
local n="$1"
if ((n <= 1)); then
echo 1
else\ns
local prev
prev=$(factorial $((n - 1)))
echo $((n * prev))
before #{ interpol
# {' '}
# comment should be fine heres s
$a / $-s + 0xFF
}s#{' '}
x
a after
fi
} #{s}
log INFO "factorial(5) = $(factorial 5)"
BASH
puts multi
# Arrays mixing everything
mixed = [
"🐍 Ruby + Python? sacrilege! 🐍",
"日本語とEnglishと🔧mix",
"Spacing test →→→→→→→",
"Zero-width joiner test: 👨‍👩‍👧‍👦 family emoji",
'🐍 Ruby + Python? sacrilege! 🐍',
'日本語とEnglishと🔧mix',
'Spacing test →→→→→→→',
'Zero-width joiner test: 👨‍👩‍👧‍👦 family emoji'
]
two_docs = <<DOC1 , <<DOC2
stuff for doc2
rdvajehvbaejbfh
two_docs = <<~DOC1, <<~DOC2
stuff for doc2
rdvajehvbaejbfh
DOC1
stuff for doc 2 with #{not interpolation} and more
stuff for doc 2 with #{!interpolation} and more
DOC2
p = 0 <<22 # not a heredoc
p = 0 << 22 # not a heredoc
mixed.each { |m| puts m }
@@ -126,10 +121,10 @@ end
escaped = "Line1\nLine2\tTabbed 😀"
puts escaped
p = 0 <<2
p = 0 << 2
# Frozen string literal test
# frozen_string_literal: true
const_str = "定数文字列🔒".freeze
const_str = '定数文字列🔒'.freeze
puts const_str
# End marker
@@ -137,14 +132,12 @@ puts '--- END OF UNICODE TEST FILE ---'
# Ruby syntax highlighting test
=begin
This is a multi-line comment.
It spans multiple lines.
Good for testing highlighting.
This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped linetest,
=end
# This is a multi-line comment.
# It spans multiple lines.
# Good for testing highlighting.
#
# This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped line test, This is a wrapped linetest,
#
# Constants
@@ -153,11 +146,12 @@ MAX_ITER = 5
# Module
module Utilities
def self.random_greeting
["Hello", "Hi", "Hey", "Hola", "Bonjour", "Merhaba"].sample
%w[Hello Hi Hey Hola Bonjour Merhaba].sample
end
def self.factorial(n)
return 1 if n <= 1
n * factorial(n - 1)
end
end
@@ -218,18 +212,16 @@ end
# Method definition
def greet_person(name)
puts "#{Utilities.random_greeting}, #{name}!"
if (name == "harry")
return true
else
return "s"
end
return true if name == 'harry'
's'
end
h = a / a
# Calling methods
greet_person("Alice")
greet_person("Bob")
greet_person('Alice')
greet_person('Bob')
# Loops
i = 0
@@ -249,7 +241,7 @@ begin
rescue ZeroDivisionError => e
puts "Caught an error: #{e}"
ensure
puts "This runs no matter what"
puts 'This runs no matter what'
end
# Arrays of objects
@@ -285,7 +277,7 @@ end
end
# Special objects
so = SpecialObject.new("Special", 10)
so = SpecialObject.new('Special', 10)
puts "Double: #{so.double_value}, Triple: #{so.triple_value}"
# String interpolation and formatting
@@ -296,8 +288,8 @@ multi_line = <<~TEXT
k kmW ;
This is a multi-line string.
It spans multiple lines.
Gossn m
dd
Gossn sssmss
ddsss
od for testing highlighting.
TEXT
@@ -305,7 +297,7 @@ puts multi_line
# Symbols and strings
sym = :my_symbol == __dir__
str = "my string"
str = 'my string'
puts "Symbol: #{sym}, String: #{str}"
# Random numbers
@@ -324,25 +316,25 @@ end
# Block with yield
def wrapper
puts "Before block"
puts 'Before block'
yield if block_given?
puts "After block"
puts 'After block'
end
# ss
wrapper { puts "Inside block" }
wrapper { puts 'Inside block' }
# Sorting
sorted = rand_nums.sort
puts "Sorted: #{sorted.join(', ')}"
# Regex
sample_text = "The quick brown fox jumps over the lazy dog"
sample_text = 'The quick brown fox jumps over the lazy dog'
puts "Match 'fox'?" if sample_text =~ /fox/
# End of test script
puts "Ruby syntax highlighting test complete."
puts 'Ruby syntax highlighting test complete.'
__END__

View File

@@ -5,18 +5,21 @@
#include "utils/utils.h"
Editor *new_editor(const char *filename_arg, Coord position, Coord size,
bool unix_eol) {
uint8_t eol) {
Editor *editor = new Editor();
if (!editor)
return nullptr;
uint32_t len = 0;
std::string filename = path_abs(filename_arg);
char *str = load_file(filename.c_str(), &len);
editor->unix_eol = eol & 1;
char *str = load_file(filename.c_str(), &len, &editor->unix_eol);
if (!str) {
free_editor(editor);
return nullptr;
str = (char *)malloc(1);
*str = '\n';
len = 1;
}
editor->unix_eol = unix_eol;
if ((eol >> 1) & 1)
editor->unix_eol = eol & 1;
editor->filename = filename;
editor->uri = path_to_file_uri(filename);
editor->position = position;

View File

@@ -76,7 +76,7 @@ int main(int argc, char *argv[]) {
ruby_init();
ruby_start((get_exe_dir() + "/../config/main.rb").c_str());
ruby_start();
load_theme();
load_languages_info();
load_custom_highlighters();
@@ -84,10 +84,10 @@ int main(int argc, char *argv[]) {
Coord screen = start_screen();
const char *filename = (argc > 1) ? argv[1] : "";
bool unix_eol = read_line_endings();
uint8_t eol = read_line_endings();
Editor *editor =
new_editor(filename, {0, 0}, {screen.row - 2, screen.col}, unix_eol);
new_editor(filename, {0, 0}, {screen.row - 2, screen.col}, eol);
Bar bar(screen);
auto end = std::chrono::high_resolution_clock::now();

View File

@@ -1,5 +1,3 @@
#include "ruby/internal/gc.h"
#include "ruby/internal/value.h"
#include "scripting/decl.h"
#include "utils/utils.h"
@@ -28,16 +26,55 @@ struct R_Language {
VALUE C_module = Qnil;
std::mutex ruby_mutex;
void ruby_start(const char *main_file) {
namespace fs = std::filesystem;
static void ruby_load(const char *main_file) {
std::lock_guard lock(ruby_mutex);
ruby_init_loadpath();
int state = 0;
rb_load_protect(rb_str_new_cstr(main_file), 0, &state);
if (state) {
rb_errinfo();
VALUE err = rb_errinfo();
rb_set_errinfo(Qnil);
fprintf(stderr, "%d: Failed to load Ruby file\n", state);
return;
fprintf(stderr, "%d: Failed to load Ruby file %s\n", state, main_file);
}
}
static void ruby_eval_string(const char *code) {
int state = 0;
rb_eval_string_protect(code, &state);
if (state) {
VALUE err = rb_errinfo();
rb_set_errinfo(Qnil);
fprintf(stderr, "Ruby eval failed\n");
}
}
void ruby_start() {
fs::path exe_dir = get_exe_dir();
std::vector<fs::path> candidates;
candidates.emplace_back("./crib.rb");
const char *xdg = std::getenv("XDG_CONFIG_HOME");
const char *home = std::getenv("HOME");
if (xdg) {
candidates.emplace_back(fs::path(xdg) / "crib/crib.rb");
candidates.emplace_back(fs::path(xdg) / "crib/main.rb");
candidates.emplace_back(fs::path(xdg) / "crib.rb");
} else if (home) {
fs::path base = fs::path(home) / ".config";
candidates.emplace_back(base / "crib/crib.rb");
candidates.emplace_back(base / "crib/main.rb");
candidates.emplace_back(base / "crib.rb");
}
candidates.emplace_back(exe_dir / "../config/main.rb");
candidates.emplace_back(exe_dir / "../config/crib.rb");
ruby_eval_string(crib_module);
ruby_eval_string(tokens_def);
for (const auto &p : candidates) {
if (fs::exists(p) && fs::is_regular_file(p)) {
ruby_load(p.string().c_str());
break;
}
}
C_module = rb_const_get(rb_cObject, rb_intern("C"));
if (C_module == Qnil)
@@ -102,14 +139,16 @@ bool custom_compare(VALUE match_block, VALUE state1, VALUE state2) {
}
VALUE parse_custom(std::vector<Token> *tokens, VALUE parser_block,
const char *line, uint32_t len, VALUE state) {
const char *line, uint32_t len, VALUE state,
uint32_t c_line) {
std::lock_guard lock(ruby_mutex);
tokens->clear();
if (NIL_P(parser_block))
return {};
VALUE ruby_line = rb_str_new(line, len);
VALUE tokens_and_state_hash =
rb_funcall(parser_block, rb_intern("call"), 2, ruby_line, state);
VALUE line_idx = UINT2NUM(c_line);
VALUE tokens_and_state_hash = rb_funcall(parser_block, rb_intern("call"), 3,
ruby_line, state, line_idx);
VALUE tokens_rb =
rb_hash_aref(tokens_and_state_hash, ID2SYM(rb_intern("tokens")));
for (long i = 0; i < RARRAY_LEN(tokens_rb); ++i) {
@@ -287,12 +326,22 @@ void load_languages_info() {
lsps[lsp.command] = lsp;
}
bool read_line_endings() {
uint8_t read_line_endings() {
std::lock_guard lock(ruby_mutex);
if (C_module == Qnil)
return true;
return 1;
VALUE le = rb_funcall(C_module, rb_intern("line_endings"), 0);
if (SYMBOL_P(le))
return std::string(rb_id2name(SYM2ID(le))) == "unix";
return true;
if (!SYMBOL_P(le))
return 1;
uint8_t flags = 1;
const char *name = rb_id2name(SYM2ID(le));
if (std::strcmp(name, "unix") == 0)
flags = 0b01;
else if (std::strcmp(name, "windows") == 0)
flags = 0b00;
else if (std::strcmp(name, "auto_unix") == 0)
flags = 0b11;
else if (std::strcmp(name, "auto_windows") == 0)
flags = 0b10;
return flags;
}

View File

@@ -1,11 +1,10 @@
#include "syntax/decl.h"
#include "syntax/langs.h"
#include <cstdint>
struct BashFullState {
int brace_level = 0;
enum : uint8_t { NONE, STRING, HEREDOC };
enum : uint8_t { NONE, STRING, HEREDOC, PARAMETER };
uint8_t in_state = BashFullState::NONE;
bool line_cont = false;
@@ -50,7 +49,8 @@ bool bash_state_match(std::shared_ptr<void> state_1,
std::shared_ptr<void> bash_parse(std::vector<Token> *tokens,
std::shared_ptr<void> in_state,
const char *text, uint32_t len) {
const char *text, uint32_t len,
uint32_t line_num) {
static bool keywords_trie_init = false;
if (!keywords_trie_init) {
keywords_trie_init = true;
@@ -64,10 +64,44 @@ std::shared_ptr<void> bash_parse(std::vector<Token> *tokens,
if (len == 0)
return state;
while (i < len) {
if (state->full_state->in_state == BashFullState::PARAMETER) {
uint32_t start = i;
while (i < len) {
if (text[i] == '{') {
i++;
state->full_state->brace_level++;
continue;
}
if (text[i] == '}') {
if (--state->full_state->brace_level == 0 &&
!state->interp_stack.empty()) {
tokens->push_back({i - 1, i, TokenKind::K_INTERPOLATION});
state->full_state = state->interp_stack.top();
state->interp_stack.pop();
i++;
break;
}
}
i++;
}
continue;
}
if (state->full_state->in_state == BashFullState::STRING) {
uint32_t start = i;
while (i < len) {
if (state->full_state->lit.allow_interp && text[i] == '$') {
if (++i < len && text[i] == '{') {
tokens->push_back({start, i - 1, TokenKind::K_STRING});
tokens->push_back({i - 1, i, TokenKind::K_INTERPOLATION});
state->interp_stack.push(state->full_state);
state->full_state = std::make_shared<BashFullState>();
state->full_state->in_state = BashFullState::PARAMETER;
state->full_state->brace_level = 1;
break;
}
}
if (text[i] == state->full_state->lit.delim[0]) {
i++;
tokens->push_back({start, i, TokenKind::K_STRING});
state->full_state->in_state = BashFullState::NONE;
break;
@@ -79,7 +113,7 @@ std::shared_ptr<void> bash_parse(std::vector<Token> *tokens,
continue;
}
if (text[i] == '#') {
if (i == 0 && len > 4 && text[i + 1] == '!') {
if (line_num == 0 && i == 0 && len > 4 && text[i + 1] == '!') {
tokens->push_back({0, len, TokenKind::K_SHEBANG});
return state;
}
@@ -91,6 +125,12 @@ std::shared_ptr<void> bash_parse(std::vector<Token> *tokens,
state->full_state->lit.allow_interp = false;
tokens->push_back({i, ++i, TokenKind::K_STRING});
continue;
} else if (text[i] == '"') {
state->full_state->in_state = BashFullState::STRING;
state->full_state->lit.delim = "\"";
state->full_state->lit.allow_interp = true;
tokens->push_back({i, ++i, TokenKind::K_STRING});
continue;
}
i++;
}

View File

@@ -64,6 +64,10 @@ void Parser::work() {
dirty_lines.push(c_line);
continue;
}
if (scroll_max > 50 && c_line < scroll_max - 50) {
dirty_lines.push(c_line);
continue;
}
uint32_t line_count = line_tree.count();
lock_data.lock();
std::shared_ptr<void> prev_state =
@@ -118,13 +122,14 @@ void Parser::work() {
std::static_pointer_cast<CustomState>(prev_state);
state = state_ptr->state;
}
VALUE out_state =
parse_custom(&line_data->tokens, parser_block, text, r_len, state);
VALUE out_state = parse_custom(&line_data->tokens, parser_block, text,
r_len, state, c_line);
std::shared_ptr<CustomState> out_state_ptr =
std::make_shared<CustomState>(out_state);
new_state = out_state_ptr;
} else {
new_state = parse_func(&line_data->tokens, prev_state, text, r_len);
new_state =
parse_func(&line_data->tokens, prev_state, text, r_len, c_line);
}
line_data->in_state = prev_state;
line_data->out_state = new_state;
@@ -134,7 +139,8 @@ void Parser::work() {
}
prev_state = new_state;
c_line++;
if (c_line < line_count && c_line > scroll_max + 50) {
if (c_line < line_count && c_line > scroll_max + 50 && scroll_max < 50 &&
c_line < scroll_max + 50) {
lock_data.unlock();
if (lock.owns_lock())
lock.unlock();

View File

@@ -288,7 +288,8 @@ bool ruby_state_match(std::shared_ptr<void> state_1,
std::shared_ptr<void> ruby_parse(std::vector<Token> *tokens,
std::shared_ptr<void> in_state,
const char *text, uint32_t len) {
const char *text, uint32_t len,
uint32_t line_num) {
static bool keywords_trie_init = false;
static Trie<void> base_keywords_trie;
static Trie<void> expecting_keywords_trie;
@@ -703,7 +704,7 @@ std::shared_ptr<void> ruby_parse(std::vector<Token> *tokens,
i++;
continue;
} else if (text[i] == '#') {
if (i == 0 && len > 4 && text[i + 1] == '!') {
if (line_num == 0 && i == 0 && len > 4 && text[i + 1] == '!') {
state->full_state->expecting_expr = false;
tokens->push_back({0, len, TokenKind::K_SHEBANG});
return state;

View File

@@ -44,7 +44,7 @@ std::string get_exe_dir() {
return path.substr(0, path.find_last_of('/'));
}
char *load_file(const char *path, uint32_t *out_len) {
char *load_file(const char *path, uint32_t *out_len, bool *out_eol) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file.is_open())
return nullptr;
@@ -64,18 +64,28 @@ char *load_file(const char *path, uint32_t *out_len) {
if (!buf)
return nullptr;
file.read(buf, data_len);
if (memchr(buf, '\r', data_len) == nullptr) {
bool has_cr = memchr(buf, '\r', data_len) != nullptr;
bool has_lf = memchr(buf, '\n', data_len) != nullptr;
if (!has_cr && !has_lf) {
uint32_t write = data_len;
if (write == 0 || buf[write - 1] != '\n')
buf[write++] = '\n';
*out_len = write;
return buf;
}
if (!has_cr) {
*out_eol = true;
uint32_t write = data_len;
if (buf[write - 1] != '\n')
buf[write++] = '\n';
*out_len = write;
return buf;
}
*out_eol = false;
uint32_t write = 0;
for (uint32_t i = 0; i < data_len; ++i)
if (buf[i] != '\r')
buf[write++] = buf[i];
if (write == 0 || buf[write - 1] != '\n')
if (buf[write - 1] != '\n')
buf[write++] = '\n';
*out_len = write;
return buf;