本記事ではRust製Editor, helix において、fileの中身がどのようにrenderingされるかをソースコードから理解することを目指します。 具体的には、hx ./Cargo.toml
のようにして、renderingしたいfile pathを引数にしてhelixを実行した場合を想定しています。 今後はkeyboardからの入力をどのように処理しているかのevent handling編, LSPとどのように話しているかのLSP編, 各種設定fileや初期化処理の起動編等を予定しています。
versionは現時点でのmaster branchの最新0097e19 を対象にしています。直近のreleaseは23.03
です。
本記事では以下の点を目指します。
helixのソースコードに現れる各種コンポーネントの概要や責務の理解 Application
, Editor
, View
, Document
, Compositor
, TerminalBackend
等々..rendering処理以外でも必ず登場する型達なので、他の処理を読む際にも理解が役立ちます 引数で渡したfileの中身が最終的にどのようにterminalにrenderingされるかの処理の流れ 本記事で扱わないこと。
起動時の初期化処理 Fileの編集 Inline text(TextAnnotations
) LSPの型ヒントのように表示はされているが、操作の対象にはならないtext この処理を省くことで説明が大幅にシンプルになります もしもまだhelixをinstallされていない場合は、公式doc を確認してください。 公式docは最新release版とは別にmaster版 もあります。sourceからbuildされた際はmaster版をみるといいと思います。
sourceからbuildする方法は簡単で以下のように行います。
git clone https://github.com/helix-editor/helix
cd helix
cargo install -- path helix-term -- locked
--locked
はつけて大丈夫です。 maintenerの一人であるpascal先生がcargo updateの変更commitを一つ一つreview したりしていました。
Editorとしてフルに活用するにはこの後にruntime directoryを作成したり、利用言語のlsp server(rust-analyzer
等)を設定したりする必要がありますが、今回はそれらの機能を利用しないので、特に必要ありません。
次にhelixのbinary, hx
の使い方について簡単におさらいします。 基本的には開きたいfileを引数にして実行するだけです。
hx ./Cargo.toml
この際-v
をつけるとloggingのlevelが変わり--log
でlog fileを指定することもできます。デフォルトでは~/.cache/helix/helix.log
です。 また、fileを開く際に行数と列数を指定することもできます。
hx ./Cargo.toml:3:5
このように実行すると3行目の5文字目がフォーカスされた状態でfileを開けます。 また、複数fileをwindowの分割方法を指定して開くこともできます。
hx README.md Cargo.toml -- vsplit
その他にも--tutor
でtutorialであったり、--health
で言語毎のsyntax highlightやlspの設定状況を確認できたりしますが、今回は触れません。
Helixもinstallして起動方法も確認したので、早速main.rs
から読んでいきたいところですが、まずhelixというapplicationの起動から終了までの概要を説明させてください。 Applicationのlifecycleは大きく以下の3つに分けられます。
初期化処理 Application::new()
Application::run()
初期化処理では、引数の処理や設定fileのparse、loggingのsetup等を行います。 Application::new()
に必要な引数の準備が主な処理です。
Application
は処理のtop levelの型でfieldにeditorに必要な各種componentを保持しています。 型としては以下のように定義されています。
pub struct Application {
compositor : Compositor,
terminal : Terminal,
pub editor : Editor,
config : Arc< ArcSwap< Config> > ,
# [ allow ( dead_code ) ]
theme_loader : Arc< theme:: Loader> ,
# [ allow ( dead_code ) ]
syn_loader : Arc< syntax:: Loader> ,
signals : Signals,
jobs : Jobs,
lsp_progress : LspProgressMap,
last_render : Instant,
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/application.rs#L63
これらのfieldは処理を追っていく中で役割がわかってくるので今は特に理解しておく必要はないです。 rederingを追っていく本記事との関係ではCompositor
, Terminal
, Editor
が関わってきます。 arc_swap::ArcSwap
はhelix外の型なのですが、処理を理解するにはわかっていることが望ましいです。 helix外の重要なcrateについては別記事でまとめて扱う予定です。
一応各fieldの概要を説明しておくと
compositor: Compositor
: rederingを担う各種Componentを管理しているterminal: Terminal
: 実際にterminalへの出力や制御を担うeditor: Editor
: FileやViewの管理や履歴等の状態を保持config: Arc<ArcSwap<Config>>
: 各種設定へのaccessを提供theme_loader: Arc<theme::Loader>
: Themeへのaccessを提供syn_loader: Arc<syntax::Loader>
: syntax設定へのaccessを提供signals: Signals
: signal handlingjobs: Jobs
: 非同期のtaskを管理lsp_pregress: LspProgressMap
: LSP関連の状態を保持last_render: Instant
: render関連の状態Application::new()
の詳細については実際の処理でどのように利用されるかを見てからそれらがどう生成されたかを確認したほうがわかりやすいと思ったので後ほど戻ってきます。
Appliation
が生成できると、Application::run()
が呼ばれます。 この処理が実質的な処理の開始で、userの入力を処理して結果をrenderingするevent loopに入ります。 ここでterminalを初期化して、panic hookを設定したりするので、terminalの制御がhelixに移ります。 このevent_loopの中で今回追っていく, Application::render()
が登場します。
以上を念頭に、実際のcodeを掲載します。メンタルモデルを共有できればいいのでもろもろ割愛して載せています。 HelixではGUIも検討 されていますが、現状はterminal editorなのでmainはhelix-term/src/main.rs
にあります。
fn main ( ) -> Result < ( ) > {
let exit_code = main_impl ( ) ? ;
std:: process:: exit( exit_code) ;
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/main.rs#L37
mainはexit codeの処理のみ行います。 以下が実質的なmain処理です。
use anyhow:: { Context, Error, Result } ;
use crossterm:: event:: EventStream;
# [ tokio ::main ]
async fn main_impl ( ) -> Result < i32 > {
let args = Args:: parse_args( ) . context ( " could not parse arguments" ) ? ;
let config = { }
let syn_loader_conf = { }
let mut app = Application:: new( args, config, syn_loader_conf)
. context ( " unable to create new application" ) ? ;
let exit_code = app. run ( & mut EventStream:: new( ) ) . await? ;
Ok ( exit_code)
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/main.rs#L43
もろもろ省略するとこれまで見てきたとおり、mainではApplication::new()
を実行したのち、Application::run()
を呼んで、戻り値をexit codeとして返しています。 helixではErrorの表現として、anyhow
を利用しており、Errorを呼び出し側でhandlingしたい場合のみ専用の型を定義しています。 また本記事ではuserからのkeyboard入力等のevent handlingを扱わないので詳しくはふれませんが、crossterm::event::EventStream
がuserからの入力を表しています。 ちなみにですが、main()
以外に#[tokio::main]
annotationを使っている例を初めて見ました。
次にApplication::run()
ですが以下のようになっています。
impl Application {
pub async fn run < S> ( & mut self , input_stream : & mut S) -> Result < i32 , Error>
where
S: Stream< Item = crossterm:: Result < crossterm:: event:: Event> > + Unpin,
{
self . claim_term ( ) . await? ;
let hook = std:: panic:: take_hook( ) ;
std:: panic:: set_hook( Box :: new( move | info| {
let _ = TerminalBackend:: force_restore( ) ;
hook ( info) ;
} ) ) ;
self . event_loop ( input_stream) . await;
let close_errs = self . close ( ) . await;
self . restore_term ( ) ? ;
for err in close_errs {
self . editor. exit_code = 1 ;
eprintln! ( " Error: {} " , err) ;
}
Ok ( self . editor. exit_code)
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/application.rs#L1106
基本的にはevent loop前後の処理を実施しています。 self.claim_term()
でterminalを初期化しています。std::panic_set_hook()
を呼んでいるのはhelixの処理中にpanicするとuserのshellに綺麗に戻れなくなることを防ぐためです。
続いてevent_loop。
impl Application {
pub async fn event_loop < S> ( & mut self , input_stream : & mut S)
where
S: Stream< Item = crossterm:: Result < crossterm:: event:: Event> > + Unpin,
{
self . render ( ) . await; self . last_render = Instant:: now( ) ;
loop {
if ! self . event_loop_until_idle ( input_stream) . await {
break ;
}
}
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/application.rs#L291
まずApplication::render()
を実行して初期化したterminalにhelixのviewをrenderingしたのち、実質的なevent loopであるApplication::event_loop_until_idle()
に入ります。 というわけで、今回の主題であるApplication::render()
にたどり着きました。この処理を理解するのが本記事の目標です。
impl Application {
async fn render ( & mut self ) {
let mut cx = crate :: compositor:: Context { editor: & mut self . editor,
} ;
let area = self . terminal
. autoresize ( )
. expect ( " Unable to determine terminal size" ) ;
let surface = self . terminal. current_buffer_mut ( ) ;
self . compositor. render ( area, surface, & mut cx) ;
self . terminal. draw ( pos, kind) . unwrap ( ) ; }
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/application.rs#L255
Application::render()
の主要な処理は上記のような流れです。各処理ごとの概要を説明します。
rendering処理で引き回す情報を表現したcompositor::Context
を作ります。大事なのはrederingにEditor
にaccessできるという点です。 現在のterminalのwindow sizeを取得します。 helixが管理しているterminal rendering用のbufferを取得します。このbufferへの書き込みはすぐにterminalに反映されるのではなく5の処理で反映されます。 rendering処理の実装です。Application
はredering処理をCompositor
に委譲していることがわかります。 Compositor
によってbufferへ書き込まれた情報を実際にterminalに反映します。 5をコメントアウトすると実際になにも描画されなくなります。(:q
で抜けれます)というわけで、Application::render()
はrenderingするための準備(contextの生成、terminal sizeの取得, 書き込み先のbuffer管理)を行ったのち、Compositor
に処理を委譲して、最後にその結果をterminalに反映させているということがわかりました。 self.terminal.draw()
は後ほど見ていきます。
続いて、Compository::render()
です。処理はsimpleで
impl Compositor {
pub fn render ( & mut self , area : Rect, surface : & mut Surface, cx : & mut Context) {
for layer in & mut self . layers {
layer. render ( area, surface, cx) ;
}
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/compositor.rs#L168
自身が保持しているlayerをiterateして順番にlayer.render()
を呼び出しているだけです。
Compository
は以下のように、layersとしてdyn Component
trait objectを保持しています。
pub struct Compositor {
layers : Vec < Box < dyn Component> > ,
area : Rect,
pub ( crate ) last_picker : Option < Box < dyn Component> > ,
}
ここまでで、おそらくComponent
traitにrender()
が定義されていて、dynamic traitになっていることから、helixの各種component(editor, filepicker, buffer list, file tree,..)がComponent
を実装しているだろうということが予想できるかと思います。 また、今みている処理はhelix起動後にuserからの入力を処理する前に呼ばれているrendering処理です。となると、Compositor
が保持しているcomponentはApplication::new()
の生成処理の中でセットされていると当たりをつけることができないでしょうか。 ということで、Component
traitを実装した具体的な型を探すべく、Application::new()
に戻ります。
Application::new()
の中で行われているCompository
の生成処理を見ていきます。具体的には、Component
traitを実装しているstructを特定します。
use arc_swap:: { access:: Map, ArcSwap} ;
impl Application {
pub fn new (
args : Args,
config : Config,
syn_loader_conf : syntax:: Configuration,
) -> Result < Self , Error> {
let mut compositor = Compositor:: new( area) ;
let config = Arc:: new( ArcSwap:: from_pointee( config) ) ; let mut editor = Editor:: new(
area,
theme_loader. clone ( ) ,
syn_loader. clone ( ) ,
Arc:: new( Map:: new( Arc:: clone( & config) , | config : & Config| {
& config. editor
} ) ) ,
) ;
let keys = Box :: new( Map:: new( Arc:: clone( & config) , | config : & Config| { & config. keys
} ) ) ;
let editor_view = Box :: new( ui:: EditorView:: new( Keymaps:: new( keys) ) ) ;
compositor. push ( editor_view) ;
let app = Self {
compositor,
terminal,
editor,
config,
} ;
Ok ( app)
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/application.rs#L104
Compositor
関連の処理のみ載せています。
Compositor
を生成しています。引数のarea
は気にしなくて大丈夫です。Config
をArc
とArcSwap
でwrapしています。各種componentにそれぞれに設定を渡すためです。設定のうちkey bindに関する設定です。 key bindの設定を渡して,EditorView
componentを設定しています。 EditorView
をCompositor
にpushします。ということで、Compositor
に渡されたcomponentはEditorView
ということがわかりました。
一応、Compositor::push()
をみておくと
impl Compositor {
pub fn push ( & mut self , mut layer : Box < dyn Component> ) {
let size = self . size ( ) ;
layer. required_size ( ( size. width, size. height) ) ;
self . layers. push ( layer) ;
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/compositor.rs#L102
渡されたdyn Component
にsizeの情報を渡した後、自身のVecにpushしています。 EditorView
はrequired_size()
を実装しておらずnoopな処理なのでここでは気にしなくてよいです。
また、ここでComponent
の定義を確認しておきます。
use std:: any:: Any;
pub trait Component : Any + AnyComponent {
fn handle_event ( & mut self , _event : & Event, _ctx : & mut Context) -> EventResult {
EventResult:: Ignored( None )
}
fn should_update ( & self ) -> bool {
true
}
fn render ( & mut self , area : Rect, frame : & mut Surface, ctx : & mut Context) ;
fn cursor ( & self , _area : Rect, _ctx : & Editor) -> ( Option < Position> , CursorKind) {
( None , CursorKind:: Hidden)
}
fn required_size ( & mut self , _viewport : ( u16 , u16 ) ) -> Option < ( u16 , u16 ) > {
None
}
fn type_name ( & self ) -> & 'static str {
std:: any:: type_name:: < Self > ( )
}
fn id ( & self ) -> Option < & 'static str > {
None
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/compositor.rs#L39
AnyComponent
は気にしなくてよいです。 Renderingとの関係では、Component
の責務は渡されたSurface
(buffer)にContext
の情報を使って、reder処理を行うことです。
ということでrender処理はApplication
, Compositor
と委譲されてEditorView
が次に呼ばれることがわかりました。
impl Component for EditorView {
fn render ( & mut self , area : Rect, surface : & mut Surface, cx : & mut Context) {
surface. set_style ( area, cx. editor. theme. get ( " ui.background" ) ) ;
let mut editor_area = area. clip_bottom ( 1 ) ;
for ( view, is_focused) in cx. editor. tree. views ( ) { let doc = cx. editor. document ( view. doc) . unwrap ( ) ; self . render_view ( cx. editor, doc, view, area, surface, is_focused) ; }
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/ui/editor.rs#L1359
本来の処理はbuffer line(開いているbufferのlist)やstatus line等のrender処理があるのですが割愛しています。 それらを無視できれば、EditorView::render()
の責務はsimpleで、themeの背景色をsetしたのち、Editor
が保持しているView
のredering処理を呼ぶことだけです。
cx.editor.theme.get("ui.background")
でuserが指定したthemeの背景色用のTheme
が取得できます。仮にこの行をコメントアウトすると背景色が反映されなくなります。描画領域を下から1行減らす処理です。空いた行にstatus lineを描画します。 Editor
が保持しているView
をiterateします。is_forcus
はuserが現在focusしているかのflagでcursorを描画するかの判定等に利用します。View
に対応するDocument
を取得します。View
とDocument
については後述します。この取得が失敗するのはbugなのでunwrapです。View
のredering処理を呼び出します。ここでEditor
からView
を取得しています。Compositor
の時と同じようにまだuserの入力を処理する前の段階なので、Editor
が保持しているなんらかのView
はApplication::new()
の処理の中で生成されたと考えられます。 ということで、Editor
がView
をどのように生成したかを見ていきます。
impl Application {
pub fn new (
args : Args,
config : Config,
syn_loader_conf : syntax:: Configuration,
) -> Result < Self , Error> {
let editor_view = Box :: new( ui:: EditorView:: new( Keymaps:: new( keys) ) ) ;
compositor. push ( editor_view) ;
if args. load_tutor {
} else if ! args. files. is_empty ( ) {
let first = & args. files[ 0 ] . 0 ; if first. is_dir ( ) {
} else {
let nr_of_files = args. files. len ( ) ;
for ( i, ( file, pos) ) in args. files. into_iter ( ) . enumerate ( ) {
if file. is_dir ( ) {
return Err ( anyhow:: anyhow! (
" expected a path to file, found a directory. (to open a directory pass it as first argument)"
) ) ;
} else {
let action = match args. split {
_ if i == 0 => Action:: VerticalSplit,
Some ( Layout:: Vertical) => Action:: VerticalSplit,
Some ( Layout:: Horizontal) => Action:: HorizontalSplit,
None => Action:: Load,
} ;
let doc_id = editor
. open ( & file, action) . context ( format! ( " open '{} '" , file. to_string_lossy ( ) ) ) ? ;
let view_id = editor. tree. focus;
let doc = doc_mut! ( editor, & doc_id) ;
let pos = Selection:: point( pos_at_coords ( doc. text ( ) . slice ( .. ) , pos, true ) ) ;
doc. set_selection ( view_id, pos) ;
}
}
}
} else {
}
let app = Self {
} ;
Ok ( app)
}
}
再びApplication::new()
です。さきほどはCompository
とEditorView
の生成処理に注目しましたが、今回はその次の処理が重要です。 hx ./Cargo.toml
のように引数にopenしたfileが渡されていることを前提にして、なんやかんや判定して、1のeditor.open()
のところまで来ます。 引数のfile
はfile path, action
にはAction::VerticalSplit
が設定されます。editor.open()
の戻り値がdoc_id
となっており、次の処理でview_id
を取得していることからこの処理がDocument
およびView
の生成処理であることが予想できます。 ということで、Editor::open()
を見ていきましょう。
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/application.rs#L192
impl Editor {
pub fn open ( & mut self , path : & Path, action : Action) -> Result < DocumentId, Error> {
let path = helix_core:: path:: get_canonicalized_path( path) ? ; let id = self . document_by_path ( & path) . map ( | doc | doc. id ) ;
let id = if let Some ( id) = id {
id
} else {
let mut doc = Document:: open( & path,
None ,
Some ( self . syn_loader. clone ( ) ) ,
self . config. clone ( ) ,
) ? ;
let id = self . new_document ( doc) ; let _ = self . launch_language_server ( id) ;
id
} ;
self . switch ( id, action) ; Ok ( id)
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-view/src/editor.rs#L1310
Editor::open()
は引数にfile pathとfileの開き方(windowをvertical,horizonどちらに分割するか)をとって、対応するDocument
を生成したのち、識別子であるDocumentId
を返します。
file pathの正規化処理です。~
を展開したりします。 既にpathに該当するDocument
があるかを確かめます。ここではNone
が返ってきます。 Document
の生成処理。生成したDocument
を保持する処理です。 今回はふれませんが、ここでLSP serverを起動します。 後述します。 ということで、Document::open()
をみます
use crate :: editor:: Config;
impl Document {
pub fn open (
path : & Path,
encoding : Option < & 'static encoding:: Encoding> ,
config_loader : Option < Arc< syntax:: Loader> > ,
config : Arc< dyn DynAccess< Config> > ,
) -> Result < Self , Error> {
let ( rope, encoding) = if path. exists ( ) {
let mut file =
std:: fs:: File:: open( path) . context ( format! ( " unable to open {:?} " , path) ) ? ;
from_reader ( & mut file, encoding) ?
} else {
} ;
let mut doc = Self :: from( rope, Some ( encoding) , config) ;
Ok ( doc)
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-view/src/document.rs#L508
Document::open()
ではfileが存在する場合に、filesystemからopenしたのち、from_reader()
を呼び出しています。
pub fn from_reader < R: std:: io:: Read + ? Sized > (
reader: & mut R,
encoding: Option < & 'static encoding:: Encoding> ,
) -> Result < ( Rope, & 'static encoding:: Encoding) , Error> { }
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-view/src/document.rs#L283
from_reader()
は上記のようなsignatureをしており、reader
(file)のencodingを判定したのち、Rope
と判定したencodingを返す関数です。 この関数もとてもおもしろいので見ていきたいところなのですが、rendering処理という本題からそれてしまうので今回は飛ばします。 また、Rope
というデータ構造はhelixにおける編集対象のtextを保持する核となるデータ構造で、別の機会により詳しく述べたいと思います。 ここでは、編集対象のtext(file)を保持して、各種効率的な操作のAPIを提供してくれるデータ構造という程度に理解します。 crateとしてはropey を利用しています。
Document::from
はDocument
のconstruct処理です。
impl Document {
pub fn from (
text : Rope,
encoding : Option < & 'static encoding:: Encoding> ,
config : Arc< dyn DynAccess< Config> > ,
) -> Self {
let encoding = encoding. unwrap_or ( encoding:: UTF_8 ) ;
let changes = ChangeSet:: new( & text) ;
let old_state = None ;
Self {
id: DocumentId:: default( ) ,
path: None ,
encoding,
text,
selections: HashMap:: default( ) ,
config,
}
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-view/src/document.rs#L464
Document
はいろいろな状態を保持しているのですが、renderingを追っていく上で抑えてほしいのはfileの内容をRope
で保持していることです。 Selection
はcursorの位置を表現しています。この実装からhelixにおいてはcursorの現在位置と選択範囲がSelection
で表現されていることがわかります。 Selection
はrederingに関わってくるのでのちほどもう少し詳しく説明します。
pub struct Document {
pub ( crate ) id : DocumentId,
text : Rope,
selections : HashMap< ViewId, Selection> ,
path : Option < PathBuf> ,
encoding : & 'static encoding:: Encoding,
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-view/src/document.rs#L118
ということで、Document
の生成を確認しました。要はfileからopenしたRope
と、helix的に管理したい状態を保持しているのがDocument
というデータ構造ということが今のところわかりました。 今みているところを再掲すると
impl Editor {
pub fn open ( & mut self , path : & Path, action : Action) -> Result < DocumentId, Error> {
let id = if let Some ( id) = id {
id
} else {
let mut doc = Document:: open( & path,
None ,
Some ( self . syn_loader. clone ( ) ) ,
self . config. clone ( ) ,
) ? ;
let id = self . new_document ( doc) ; let _ = self . launch_language_server ( id) ;
id
} ;
self . switch ( id, action) ; Ok ( id)
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-view/src/editor.rs#L1310
Document::open()
から戻ってきて、self.new_document()
が次の処理です。
pub struct Editor {
pub next_document_id : DocumentId,
pub documents : BTreeMap< DocumentId, Document> ,
}
impl Editor {
fn new_document ( & mut self , mut doc : Document) -> DocumentId {
let id = self . next_document_id;
self . next_document_id =
DocumentId( unsafe { NonZeroUsize:: new_unchecked( self . next_document_id. 0. get ( ) + 1 ) } ) ;
doc. id = id;
self . documents. insert ( id, doc) ;
id
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-view/src/editor.rs#L1274
Editor::new_document()
はDocumentId
を採番して、Document
にsetしたのち、Editor
のBTreeMap
に保持しています。 今回はLSPについてはふれませんが、このタイミングでlanguage serverを起動していることから、helixではfileを開いた際に初めてfileに対応するlanguage serverを起動していることがわかります。 Editor::open()
の処理のうち、Documentを
生成して、Editor
に登録したあとはEditor::switch()
を実行して終わりです。 まだView
が出てきていないのでおそらくこの処理で、生成したEditor
に対応するView
を作るのだろうということが予想できます。
pub struct Editor {
pub tree : Tree,
}
impl Editor {
pub fn switch ( & mut self , id : DocumentId, action : Action) {
match action {
Action:: HorizontalSplit | Action:: VerticalSplit => {
let view = self
. tree
. try_get ( self . tree. focus)
. filter ( | v | id == v. doc ) . cloned ( )
. unwrap_or_else ( | | View:: new( id, self . config ( ) . gutters. clone ( ) ) ) ; let view_id = self . tree. split ( view,
match action {
Action:: HorizontalSplit => Layout:: Horizontal,
Action:: VerticalSplit => Layout:: Vertical,
_ => unreachable! ( ) ,
} ,
) ;
let doc = doc_mut! ( self , & id) ; doc. ensure_view_init ( view_id) ; }
}
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-view/src/editor.rs#L1180
なにやらView
を生成していそうな感じがあります。
まずself.tree
はTree
を参照します。ここでは詳しく述べれないのですがView
を管理している木構造です。基本的に新しいView
を作るには現在のwindowを分割していくので、これを木構造で表現しています。現在の処理ではunwrap_or_else()
のelseに入ってView::new()
が呼ばれます。 生成したView
をTree
に登録します。 doc_mut!()
はEditor
からDocumentId
に対応するDocument
を取得するhelper macroです。cursorをfileの先頭にsetする処理という理解で大丈夫です。 # [ derive ( Clone ) ]
pub struct View {
pub id : ViewId,
pub offset : ViewPosition,
pub area : Rect,
pub doc : DocumentId,
pub gutters : GutterConfig,
}
impl View {
pub fn new ( doc : DocumentId, gutters : GutterConfig) -> Self {
Self {
id: ViewId:: default( ) ,
doc,
offset: ViewPosition {
anchor: 0 ,
horizontal_offset: 0 ,
vertical_offset: 0 ,
} ,
area: Rect:: default( ) , gutters,
}
}
}
View
は上記のように定義されています。もっと多くのfieldが実際にはありますが、本記事で関連するfieldは上記です。 offset: ViewPosition
はDocument
のどこを見ているかを表すstructです。 area: Rect
はterminalに描画される範囲です。今みているView
をvertical/horizonどちらで分割するかで変わってくるので、生成時にはわからずTree
にinsertされる際に計算されるということがコメントに書いてあります。
self.tree.split()
はhelix-view::Tree
にView
を登録する処理です。Tree
の実装もおもしろいのですがrenderingの話から逸れるので、Viewの分割を表現したデータ構造に登録するくらいの理解で次にいきます。
あらためて現在位置を振り返ると、今はApplication::new()
の中で行われている引数のfileを処理する箇所を確認していました。 どうしてその処理を確認していたかというと、Application
-> Compositor
-> EditorView
とよばれたrender()
の中で、下記のようにeditor.tree.views()
でView
がiterateされているがこれがどこ生成されたかを確認するためでした。
impl Component for EditorView {
fn render ( & mut self , area : Rect, surface : & mut Surface, cx : & mut Context) {
surface. set_style ( area, cx. editor. theme. get ( " ui.background" ) ) ;
let mut editor_area = area. clip_bottom ( 1 ) ;
for ( view, is_focused) in cx. editor. tree. views ( ) { let doc = cx. editor. document ( view. doc) . unwrap ( ) ; self . render_view ( cx. editor, doc, view, area, surface, is_focused) ;
}
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/ui/editor.rs#L1359
ということで、cx.editor.tree.views()
でiterateされるview
はhx ./Cargo.toml
で起動時に渡したfileに対応するView
であることがわかりました。
のeditor.document()
はView
に対応するDocument
を取得する処理です。View
はDocumentId
を保持しており、Editor
はDocument
をBTreeMap<DocumentId, Document>
で管理しているので、対応するDocument
は常に取得できます。 is_forcused
はtrueが入っています。 いよいよredering処理の実質的な処理に近づいてきました。 実際の処理ではもっと色々なことをやっていますが、ここで抑えたいのは、syntaxやselectionの情報からBox<dyn Iterator<Item = HighlightEvent>>
を生成しているという点です。 Helixにおいてsyntax highlightやcursor, selectionがどのようにrenderingされているかが今回伝えたいおもしろい箇所なので、ここは後で細かくふれます。 また、残念ながらtext_annotations
やLineDecoration
, TranslatedPosition
にはふれません。 理由としてはこれをなしにしても本記事が十分長くなることに加えて、自分もまだ理解しきれておらず、LSPの話をしたあとのほうがわかりやすいかなと思ったからです。 (redering処理の理解においてここは避けては通れない箇所なのでいずれ戻ってきたい)
ということで次はreder_document()
です。
impl EditorView {
pub fn render_view (
& self ,
editor : & Editor,
doc : & Document,
view : & View,
viewport : Rect,
surface : & mut Surface,
is_focused : bool ,
) {
let inner = view. inner_area ( doc) ;
let area = view. area;
let theme = & editor. theme;
let config = editor. config ( ) ;
let text_annotations = view. text_annotations ( doc, Some ( theme) ) ;
let mut line_decorations: Vec < Box < dyn LineDecoration> > = Vec :: new( ) ;
let mut translated_positions: Vec < TranslatedPosition> = Vec :: new( ) ;
let mut highlights =
Self :: doc_syntax_highlights( doc, view. offset. anchor, inner. height, theme) ;
let highlights: Box < dyn Iterator < Item = HighlightEvent> > = if is_focused {
let highlights = syntax:: merge(
highlights,
Self :: doc_selection_highlights(
editor. mode ( ) ,
doc,
view,
theme,
& config. cursor_shape,
) ,
) ;
} else {
Box :: new( highlights)
} ;
render_document (
surface,
inner,
doc,
view. offset,
& text_annotations,
highlights,
theme,
& mut line_decorations,
& mut translated_positions,
) ;
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/ui/editor.rs#L78
pub fn render_document (
surface : & mut Surface,
viewport : Rect,
doc : & Document,
offset : ViewPosition,
doc_annotations : & TextAnnotations,
highlight_iter : impl Iterator < Item = HighlightEvent> ,
theme : & Theme,
line_decoration : & mut [Box < dyn LineDecoration + '_ > ],
translated_positions : & mut [TranslatedPosition],
) {
let mut renderer = TextRenderer:: new( surface, doc, theme, offset. horizontal_offset, viewport) ;
render_text (
& mut renderer,
doc. text ( ) . slice ( .. ) ,
offset,
& doc. text_format ( viewport. width, Some ( theme) ) ,
doc_annotations,
highlight_iter,
theme,
line_decoration,
translated_positions,
)
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/ui/document.rs#L94
ここはほぼ委譲しているだけです。 TextRenderer
は使う際に見ていきます。
render_text()
にてついにbufferへの書き込みを行います。
まず引数についてです
pub fn render_text < 't > (
renderer : & mut TextRenderer,
text : RopeSlice< 't > ,
offset : ViewPosition,
text_fmt : & TextFormat,
text_annotations : & TextAnnotations,
highlight_iter : impl Iterator < Item = HighlightEvent> ,
theme : & Theme,
line_decorations : & mut [Box < dyn LineDecoration + '_ > ],
translated_positions : & mut [TranslatedPosition],
) { }
TextRenderer
はrendering時のcontextと各種設定を保持しています。具体的には以下のように定義されています。
# [ derive ( Debug ) ]
pub struct TextRenderer < 'a > {
pub surface : & 'a mut Surface,
pub text_style : Style,
pub whitespace_style : Style,
pub indent_guide_char : String,
pub indent_guide_style : Style,
pub newline : String,
pub nbsp : String,
pub space : String,
pub tab : String,
pub virtual_tab : String,
pub indent_width : u16 ,
pub starting_indent : usize ,
pub draw_indent_guides : bool ,
pub col_offset : usize ,
pub viewport : Rect,
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/ui/document.rs#L307
text: RopeSlice<'t>
はrendering対象のtextです。 RopeSlice
はdoc にある通り、An immutable view into part of a Rope です。 offset: ViewPosition
はDocument
のうち現在のView
が表示している範囲についての情報です。scrollするとここが変わります。 text_fmt: &TextFormat
とtext_annotations: &TextAnnotations
は今回は気にしなくてよいです。 highlight_iter: impl Iterator<Item = HighlightEvent>
はさきほどふれた、syntax highlightやcursorの情報をiterateしてくれます。redner_text()
でこの情報をrenderingする際に適用するstyleに変換します。 theme: &Theme
は適用するthemeです。 line_decorations: &mut [Box<dyn LineDecorations + '_]
とtranslated_positions: &mut [TranslatedPosition]
も飛ばします。
pub fn render_text < 't > (
renderer : & mut TextRenderer,
text : RopeSlice< 't > ,
offset : ViewPosition,
text_fmt : & TextFormat,
text_annotations : & TextAnnotations,
highlight_iter : impl Iterator < Item = HighlightEvent> ,
theme : & Theme,
line_decorations : & mut [Box < dyn LineDecoration + '_ > ],
translated_positions : & mut [TranslatedPosition],
) {
let (
Position {
row: mut row_off, ..
} ,
mut char_pos, ) = visual_offset_from_block (
text,
offset. anchor,
offset. anchor,
text_fmt,
text_annotations,
) ;
let ( mut formatter, mut first_visible_char_idx) =
DocumentFormatter:: new_at_prev_checkpoint( text, text_fmt, text_annotations, offset. anchor) ; let mut styles = StyleIter { text_style: renderer. text_style,
active_highlights: Vec :: with_capacity( 64 ) ,
highlight_iter,
theme,
} ;
let mut style_span = styles . next ( )
. unwrap_or_else ( | | ( Style:: default( ) , usize :: MAX ) ) ;
loop {
let Some ( ( grapheme, mut pos) ) = formatter. next ( ) else { break ;
} ;
if pos. row as u16 >= renderer. viewport. height { break ;
}
if char_pos >= style_span. 1 { style_span = styles. next ( ) . unwrap_or ( ( Style:: default( ) , usize :: MAX ) ) ;
}
char_pos += grapheme. doc_chars ( ) ;
let grapheme_style = if let GraphemeSource:: VirtualText { highlight } = grapheme. source { } else {
style_span. 0
} ;
let virt = grapheme. is_virtual ( ) ;
renderer. draw_grapheme ( grapheme. grapheme,
grapheme_style,
virt,
& mut last_line_indent_level,
& mut is_in_indent_area,
pos,
) ;
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/ui/document.rs#L154
例に漏れずもっといろいろな処理あるのですが、fileの内容を表示することだけに絞るとreder_text()
は概ね上記のような処理です。
visual_offset_from_block()
はviewのoffset情報からrenderingを開始するのはtext上の何文字目(char_pos
)を返します。DocumentFormatter
を生成します。このstructは最終的にrenderingすべき文字をiterateしてくれるstructです。 first_visible_char_idx
は気にしなくて良いです。StyleIter
はthemeとHilightEvent
の情報から最終的に適用するstyleをiterateしてくれるstructです。最初に適用するstyleを取得。もしなければそのまま表示するので、Style::default()
を使います。 次にrenderingする"文字"です。graphemeについては後述します。Noneが返るとrederingすべき文字をすべてbufferに書いたことになるので、terminalへの反映フェーズに入ります。 renderer.viewport.height
は今開いているViewの高さなので、それ以降はrederingしても表示できないので、処理を終えます。StyleIter
はtype Item = (Style, usize)
のIterator
を実装しています。 usizeは適用すべきstyleの有効範囲です。この値と現在renderingしている文字(char_pos
)を比較して, 有効範囲外にでたら次に適用すべきstyleを取得します。いわゆる1文字とcharは1:1に対応しないので、ここで調整します。これを行わないとemoji等でおかしくなります。 VirtualText
の話は飛ばします。LSPの型hint等のことです。rederingする文字を処理します。 次の処理をみる前にgraphemeについて述べます。 Unicode Demystified によりますと
A grapheme cluster is a sequence of one or more Unicode code points that should be treated as a single unit by various processes: Text-editing software should generally allow placement of the cursor only at grapheme cluster boundaries. Clicking the mouse on a piece of text should place the insertion point at the nearest grapheme cluster boundary, and the arrow keys should move forward and back one grapheme cluster at a time.
Unicode的にはgrapheme cluster正式名称らしく、textを扱うsoftwareにおけるboundaryを提供するという理解です。 Helixを機にunicodeについても知りたいのですが、オライリーで検索しても2002や2006年ごろの本しかヒットせずでした。(おすすめのドキュメント等ご存知の方おられましたら教えて下さい)
コメントにある通り、grapheme
をbufferに書き込む処理です。 最初は1文字書き込むごとに関数呼ぶのかと思ったりしました。
impl < 'a > TextRenderer < 'a > {
pub fn draw_grapheme (
& mut self ,
grapheme : Grapheme,
mut style : Style,
is_virtual : bool ,
last_indent_level : & mut usize ,
is_in_indent_area : & mut bool ,
position : Position,
) {
let width = grapheme. width ( ) ;
let space = if is_virtual { " " } else { & self . space } ;
let nbsp = if is_virtual { " " } else { & self . nbsp } ;
let tab = if is_virtual {
& self . virtual_tab
} else {
& self . tab
} ;
let grapheme = match grapheme { Grapheme:: Tab { width } => {
let grapheme_tab_width = char_to_byte_idx ( tab, width) ;
& tab[ .. grapheme_tab_width]
}
Grapheme:: Other { ref g } if g == " " => space,
Grapheme:: Other { ref g } if g == " \u{00A0} " => nbsp,
Grapheme:: Other { ref g } => g,
Grapheme:: Newline => & self . newline,
} ;
let in_bounds = self . col_offset <= position. col && position. col < self . viewport. width as usize + self . col_offset;
if in_bounds { self . surface. set_string (
self . viewport. x + ( position. col - self . col_offset) as u16 ,
self . viewport. y + position. row as u16 ,
grapheme,
style,
) ;
} else if cut_off_start != 0 && cut_off_start < width {
}
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-term/src/ui/document.rs#L395
tabをspaceに変換したりする処理はここで行っている様です。
ここで最終的にrenderingする文字が決まります。let grapheme
の型は&str
です。 columnのoffsetに応じた判定です。この処理で横に長い行で横にscrollした際の挙動が実現されています。 ここでついにbufferに文字を書き込みます。self.surface
(buffer)には絶対位置を指示する必要があるので、現在のView
の位置(self.viewport
)を基準にします。 self.surface
は&'a mut Surface
でuse tui::buffer::Buffer as Surface;
されているので、実態はbufferです。 どこから来ているかというと、Application::render()
の中で、let surface = self.terminal.current_buffer_mut();
しており、処理の最初からずっと引き回されついにここで使われます。
ここで、helixがterminalどのように制御しているかについて説明します。 まずApplication
が保持しているTerminal
は以下のように定義されています。
# [ derive ( Debug ) ]
pub struct Terminal < B>
where
B: Backend,
{
backend : B,
buffers : [Buffer; 2],
current : usize ,
cursor_kind : CursorKind,
viewport : Viewport,
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-tui/src/terminal.rs#L52
Terminal
はterminalを操作するための型です。trait boundのBackend
にterminalへの必要な操作が定義されています。 実行時にはCrosstermBackend
がBackend
として利用されます。integration test時にはmockに差し替えられるようになっています。
Buffer
はhelixが管理するBackend
にrenderingするbufferです。なぜ、Arrayで2つ保持しているかはこのあとの処理をみていくとわかります。 Viewport
はterminalのwidth/heightを保持しています。実質的にhelix全体の描画領域です。
# [ derive ( Debug, Default, Clone, PartialEq, Eq ) ]
pub struct Buffer {
pub area : Rect,
pub content : Vec < Cell> ,
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-tui/src/buffer.rs#L123
bufferのrendering領域の情報と実際にrederingするCell
を保持しています。
# [ derive ( Debug, Default, Clone, Copy, Hash, PartialEq, Eq ) ]
pub struct Rect {
pub x : u16 ,
pub y : u16 ,
pub width : u16 ,
pub height : u16 ,
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-view/src/graphics.rs#L90
# [ derive ( Debug, Clone, PartialEq, Eq ) ]
pub struct Cell {
pub symbol : String,
pub fg : Color,
pub bg : Color,
pub underline_color : Color,
pub underline_style : UnderlineStyle,
pub modifier : Modifier,
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-tui/src/buffer.rs#L10
Cell
はterminalの1描画領域を表しており、symbol
がその文字です。defaultのthemeだとmodofier
にREVERSED
が設定されていて、背景とtextの色を判定させていたりします。
Helixがterminalに求める操作としてのBackend
traitは以下のように定義されています。
pub trait Backend {
fn claim ( & mut self , config : Config) -> Result < ( ) , io:: Error> ;
fn reconfigure ( & mut self , config : Config) -> Result < ( ) , io:: Error> ;
fn restore ( & mut self , config : Config) -> Result < ( ) , io:: Error> ;
fn force_restore ( ) -> Result < ( ) , io:: Error> ;
fn draw < 'a , I> ( & mut self , content : I) -> Result < ( ) , io:: Error>
where
I: Iterator < Item = ( u16 , u16 , & 'a Cell) > ;
fn hide_cursor( & mut self) -> Result < ( ) , io:: Error> ;
fn show_cursor( & mut self, kind: CursorKind) -> Result < ( ) , io:: Error> ;
fn get_cursor( & mut self) -> Result < ( u16 , u16 ) , io:: Error> ;
fn set_cursor( & mut self, x: u16 , y: u16 ) -> Result < ( ) , io:: Error> ;
fn clear( & mut self) -> Result < ( ) , io:: Error> ;
fn size( & self) -> Result < Rect, io:: Error> ;
fn flush( & mut self) -> Result < ( ) , io:: Error> ;
}
いくつかありますが、着目してもらいたいのがdraw()
です。
pub trait Backend {
fn draw < 'a , I> ( & mut self , content : I) -> Result < ( ) , io:: Error>
where
I: Iterator < Item = ( u16 , u16 , & 'a Cell) > ;
}
u16はXY座標で、指定の位置にCell
を描画するというapiになっています。Component::render()
によって、Buffer
のCell
を描画内容でうめたのち、このdraw()
を呼び出してterminalに反映させます。
もう一度、Application::render()
を思い出します。
impl Application {
async fn render ( & mut self ) {
let mut cx = crate :: compositor:: Context {
editor: & mut self . editor,
} ;
let area = self
. terminal
. autoresize ( )
. expect ( " Unable to determine terminal size" ) ;
let surface = self . terminal. current_buffer_mut ( ) ;
self . compositor. render ( area, surface, & mut cx) ;
self . terminal. draw ( pos, kind) . unwrap ( ) ; }
}
self.terminal.current_buffer_mut()
でbufferを取得したのち、compositor.render()
でbufferの内容を埋めます。 そして、最後に、self.terminal.draw()`でbufferの内容を反映します。
impl
impl < B> Terminal < B>
where
B: Backend,
{
pub fn current_buffer_mut ( & mut self ) -> & mut Buffer {
& mut self . buffers[ self . current]
}
}
Terminal::current_buffer_mut()
は2つ保持しているBuffer
の片方を返します。
impl < B> Terminal < B>
where
B: Backend,
{
pub fn draw (
& mut self ,
cursor_position : Option < ( u16 , u16 ) > ,
cursor_kind : CursorKind,
) -> io:: Result < ( ) > {
self . flush ( ) ? ;
self . buffers[ 1 - self . current] . reset ( ) ;
self . current = 1 - self . current;
self . backend. flush ( ) ? ;
Ok ( ( ) )
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-tui/src/terminal.rs#L177
引数のcursorはここでは気にしません。 処理の概要としてはTerminal::flush()
を呼んだ後、次に利用するBuffer
をresetして切り替えています。self.current
の初期値は0です。 最後にterminal backend(crossterm)のflushを呼んでredering処理は完了です。
impl < B> Terminal < B>
where
B: Backend,
{
pub fn flush ( & mut self ) -> io:: Result < ( ) > {
let previous_buffer = & self . buffers[ 1 - self . current] ;
let current_buffer = & self . buffers[ self . current] ;
let updates = previous_buffer. diff ( current_buffer) ;
self . backend. draw ( updates. into_iter ( ) )
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-tui/src/terminal.rs#L149
ここで初めてTerminal
がBuffer
を2つ保持していた理由がわかりました。 Buffer
をすべてbackendに渡すのではなく、前回のbufferとの差分をBuffer.diff()
で取得したのちに、self.backend.draw()
を呼び出しています。
impl Buffer {
pub fn diff < 'a > ( & self , other : & 'a Buffer) -> Vec < ( u16 , u16 , & 'a Cell) > {
let previous_buffer = & self . content;
let next_buffer = & other. content;
let width = self . area. width;
let mut updates: Vec < ( u16 , u16 , & Cell) > = vec! [ ] ;
let mut invalidated: usize = 0 ;
let mut to_skip: usize = 0 ;
for ( i, ( current, previous) ) in next_buffer. iter ( ) . zip ( previous_buffer. iter ( ) ) . enumerate ( ) {
if ( current != previous || invalidated > 0 ) && to_skip == 0 {
let x = ( i % width as usize ) as u16 ;
let y = ( i / width as usize ) as u16 ;
updates. push ( ( x, y, & next_buffer[ i] ) ) ;
}
let current_width = current. symbol. width ( ) ;
to_skip = current_width. saturating_sub ( 1 ) ;
let affected_width = std:: cmp:: max( current_width, previous. symbol. width ( ) ) ;
invalidated = std:: cmp:: max( affected_width, invalidated) . saturating_sub ( 1 ) ;
}
updates
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-tui/src/buffer.rs#L619
Buffer::diff()
はbufferの差分を計算して、更新のある必要なCell
を返す処理です。 初回は背景色を塗っているのでBuffer
の全Cell
が更新されます。 その後は例えば、cursorを一つ動かす場合、前回cursorがあった文字と現在cursorがある位置, status lineのcursor位置の表示で3 Cell
のみの更新となります。 基本的にはpreviousとnextをzipして、比較して差分を検出しています。(invalidated
がどんな場合を念頭に置いているかイマイチまだ理解できていないです)
impl < W> Backend for CrosstermBackend < W>
where
W: Write,
{
fn draw < 'a , I> ( & mut self , content : I) -> io:: Result < ( ) >
where
I: Iterator < Item = ( u16 , u16 , & 'a Cell) > ,
{
let mut fg = Color:: Reset;
let mut bg = Color:: Reset;
let mut underline_color = Color:: Reset;
let mut underline_style = UnderlineStyle:: Reset;
let mut modifier = Modifier:: empty( ) ;
let mut last_pos: Option < ( u16 , u16 ) > = None ;
for ( x, y, cell) in content {
if ! matches! ( last_pos, Some ( p) if x == p. 0 + 1 && y == p. 1 ) {
map_error ( queue! ( self . buffer, MoveTo( x, y) ) ) ? ;
}
last_pos = Some ( ( x, y) ) ;
if cell. modifier != modifier {
let diff = ModifierDiff {
from: modifier,
to: cell. modifier,
} ;
diff. queue ( & mut self . buffer) ? ;
modifier = cell. modifier;
}
if cell. fg != fg {
let color = CColor:: from( cell. fg) ;
map_error ( queue! ( self . buffer, SetForegroundColor( color) ) ) ? ;
fg = cell. fg;
}
if cell. bg != bg {
let color = CColor:: from( cell. bg) ;
map_error ( queue! ( self . buffer, SetBackgroundColor( color) ) ) ? ;
bg = cell. bg;
}
let mut new_underline_style = cell. underline_style;
if self . capabilities. has_extended_underlines {
if cell. underline_color != underline_color {
let color = CColor:: from( cell. underline_color) ;
map_error ( queue! ( self . buffer, SetUnderlineColor( color) ) ) ? ;
underline_color = cell. underline_color;
}
} else {
match new_underline_style {
UnderlineStyle:: Reset | UnderlineStyle:: Line => ( ) ,
_ => new_underline_style = UnderlineStyle:: Line,
}
}
if new_underline_style != underline_style {
let attr = CAttribute:: from( new_underline_style) ;
map_error ( queue! ( self . buffer, SetAttribute( attr) ) ) ? ;
underline_style = new_underline_style;
}
map_error ( queue! ( self . buffer, Print( & cell. symbol) ) ) ? ;
}
map_error ( queue! (
self . buffer,
SetUnderlineColor( CColor:: Reset) ,
SetForegroundColor( CColor:: Reset) ,
SetBackgroundColor( CColor:: Reset) ,
SetAttribute( CAttribute:: Reset)
) )
}
}
https://github.com/helix-editor/helix/blob/0097e191bb9f9f144043c2afcf04bc8632021281/helix-tui/src/backend/crossterm.rs#L191
ここは完全にcrossterm 側の処理になります。 概要としてはCell
の情報をcrossterm用の情報に変換したのちに、crosstermの処理を実行します。 queue!
でstd::io::Writeとcrosstermのコマンドを渡すと実行されます。 自分がはまったのは写経している際に、MoveTo(x,y)
を書いておらず、helixとcrosstermでcursorの処理がズレてしまい、cursorが2つ出現するというバグを発生させてしまったことです。
ということでhelixのrendering処理を追っていきました。 実際の処理はここに載せた処理より遥かに多くのことをしていますが、掲載したコードpathを通ると、実際にfileがrenderingされることは写経しながら確かめました。 コードを読んでいる際に、ここearly returnのし忘れじゃないかなと思うところがあり、PR を送ってみるとmergeしてもらえてうれしかったです。 最後にrendering処理のsequence diagramを載せておきます。Render sequence
(HighlightEvent iteratorとStyleIterの関係は力尽きてしまったのでそのうち書きます)