實(shí)戰(zhàn)指南:Scopes 體系、優(yōu)先級(jí)規(guī)則與自動(dòng)化測(cè)試驗(yàn)證)
Helix 高亮查詢highlights.scm實(shí)戰(zhàn)指南Scopes 體系、優(yōu)先級(jí)規(guī)則與自動(dòng)化測(cè)試驗(yàn)證【免費(fèi)下載鏈接】helixA post-modern modal text editor.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/he/helix本文基于 Helix 官方手冊(cè)中的高亮查詢指南完整講解highlights.scm查詢文件的編寫方法如何為語(yǔ)法樹節(jié)點(diǎn)分配 highlight scopefunction、type、keyword等、如何正確使用; inherits跨語(yǔ)言復(fù)用查詢、如何理解同跨度后者勝 / 嵌套節(jié)點(diǎn)最內(nèi)層勝兩條優(yōu)先級(jí)規(guī)則以及如何用cargo xtask query-check與cargo xtask highlight-check對(duì)查詢進(jìn)行語(yǔ)法校驗(yàn)和基于 caret 斷言的優(yōu)先級(jí)回歸測(cè)試。讀完本篇你能夠?yàn)槿我庹Z(yǔ)言貢獻(xiàn)或修改高亮查詢并掌握捕獲點(diǎn)選擇與驗(yàn)證的完整工作流。什么是高亮查詢從語(yǔ)法樹到主題色的映射鏈highlights.scm查詢負(fù)責(zé)把 tree-sitter 語(yǔ)法樹中的節(jié)點(diǎn)與一個(gè)highlight scope如function、type、keyword關(guān)聯(lián)起來(lái)主題theme再把每個(gè) scope 映射為具體顏色。這是每一門語(yǔ)言都必需的一個(gè)查詢文件——沒(méi)有它編輯器就無(wú)法對(duì)該語(yǔ)言做任何語(yǔ)法著色。貢獻(xiàn) Helix 語(yǔ)言支持時(shí)查詢文件必須放在固定位置runtime/queries/{language}/highlights.scm例如 Rust 語(yǔ)言的高亮查詢就位于 runtime/queries/rust/highlights.scm。整個(gè)映射鏈可以概括為語(yǔ)法樹節(jié)點(diǎn) --(highlights.scm 捕獲 scope)-- 捕獲名 --(主題 toml 的 scope→style)-- 顏色/修飾符主題的 scope 到樣式的解析規(guī)則是最長(zhǎng)匹配若一個(gè)捕獲名是function.builtin.static而主題中同時(shí)定義了function.builtin和function則使用更長(zhǎng)的function.builtin鍵。Scopes 體系選擇最具體的捕獲完整的 scope 清單及其用途記錄在手冊(cè)的主題頁(yè)book/src/themes.md 的 Scopes 一節(jié)該清單與 Sublime Text 的 scope 命名體系大體一致也參考了 TextMate scopes。核心語(yǔ)法高亮 scope 的組織結(jié)構(gòu)如下取自主題文檔的完整列表attribute— 類屬性、HTML 標(biāo)簽屬性type— 類型builtin— 語(yǔ)言內(nèi)置原始類型int、usizeparameter— 泛型類型參數(shù)Tenumvariant— 枚舉變體constructor— 構(gòu)造器、結(jié)構(gòu)體/記錄字面量、值位置的類型名constantbuiltin— 語(yǔ)言內(nèi)置常量true、false、nil等booleancharacterescapenumeric— 數(shù)字integerfloatstringregexp— 正則表達(dá)式specialpathurlsymbol— Erlang/Elixir 原子、Ruby 符號(hào)、Clojure 關(guān)鍵字commentline— 單行注釋//documentation— 單行文檔注釋如 Rust 的///block— 塊注釋/* */documentation— 塊文檔注釋如/** */unused— 未使用變量與模式如_、_foovariablemutable— 可變變量Rust 中的mutbuiltin— 語(yǔ)言保留變量self、this、supermutable— 可變語(yǔ)言變量如mut selfparameter— 函數(shù)參數(shù)mutable— 可變函數(shù)參數(shù)othermember— 復(fù)合數(shù)據(jù)類型結(jié)構(gòu)體、聯(lián)合體的字段private— 使用獨(dú)特語(yǔ)法的私有字段目前僅 ECMAScript 系語(yǔ)言label— CSS 中的.class、#id等punctuationdelimiter— 逗號(hào)、冒號(hào)bracket— 括號(hào)、尖括號(hào)等special— 字符串插值括號(hào)keywordcontrolconditional—if、elserepeat—for、while、loopimport—import、exportreturnexceptionoperator—or、indirective— 預(yù)處理指令C 的#iffunction—fn、funcstorage— 描述存儲(chǔ)方式的關(guān)鍵詞type—class、function、var、letmodifier—static、mut、const、ref等存儲(chǔ)修飾符operator—||、、function— 函數(shù)定義與調(diào)用public— 公共函數(shù)定義builtin— 語(yǔ)言內(nèi)置函數(shù)method— 方法定義與調(diào)用obj.method()public— 公共方法定義private— 私有方法獨(dú)特語(yǔ)法目前僅 ECMAScript 系macro— 宏調(diào)用Rust 的println!special— C 的預(yù)處理器tag— HTML 標(biāo)簽如bodybuiltinnamespace— 模塊與命名空間std::collections、包名special— Rust 的derive、picker 中加粗的查詢匹配項(xiàng)等markup—heading含marker與16各級(jí)標(biāo)題、listunnumbered/numbered/checked/unchecked、bold、italic、strikethrough、linkurl/label/text、quote、rawinline/blockdiff— 版本控制變更plus— 新增含gutter邊欄指示minus— 刪除含gutterdelta— 修改moved重命名/移動(dòng)、conflict沖突、gutterembedded— 嵌入在字符串模板中的插值表達(dá)式${…}選擇原則匹配能準(zhǔn)確描述該節(jié)點(diǎn)的最具體 scope。官方手冊(cè)給出的典型例子一次方法調(diào)用應(yīng)捕獲為function.method而不是籠統(tǒng)的function一次普通的字段訪問(wèn)沒(méi)有調(diào)用應(yīng)捕獲為variable.other.member。主題文檔中另有用于編輯器界面的 scope 體系ui.background、ui.cursor.*、ui.statusline.*、ui.menu.*、ui.virtual.*、diagnostic.*等以及 popup/幫助窗口中使用的markup.normal.completion、markup.heading.hover等接口 scope完整鍵值表同樣見(jiàn) book/src/themes.md。這些是主題側(cè)消費(fèi)的 scope與highlights.scm中面向語(yǔ)法高亮的 scope 屬同一套命名空間編寫主題時(shí)可一并參考??缯Z(yǔ)言復(fù)用; inherits:機(jī)制一個(gè)查詢文件可以在第一行通過(guò); inherits: lang聲明復(fù)用另一門語(yǔ)言的查詢避免為派生語(yǔ)言重復(fù)編寫整套捕獲。Helix 倉(cāng)庫(kù)中 JavaScript 系語(yǔ)言的繼承鏈就是典型示例runtime/queries/typescript/highlights.scm 第 3 行聲明; inherits: ecma,_typescriptruntime/queries/tsx/highlights.scm 第 3 行聲明; inherits: ecma,_typescript,_jsx。也就是說(shuō)tsx繼承typescript而typescript又繼承公共的ecma基礎(chǔ)查詢帶下劃線的目錄名_typescript、_jsx表示中間產(chǎn)物層的共享查詢見(jiàn) runtime/queries/ecma/README.md 說(shuō)明。繼承有一個(gè)重要約束被繼承的文件會(huì)針對(duì)每一個(gè)繼承它的語(yǔ)法分別編譯因此文件中的每一個(gè)捕獲都必須在這些語(yǔ)法中同樣合法。例如ecma層的查詢要同時(shí)能被typescript、javascript、tsx等語(yǔ)法解析任何只針對(duì)單一語(yǔ)法的節(jié)點(diǎn)名都不能寫進(jìn)共享層。優(yōu)先級(jí)規(guī)則兩條規(guī)則決定誰(shuí)贏得同一段文本當(dāng)多個(gè)捕獲匹配同一段文本時(shí)由以下兩條規(guī)則決定最終生效的 scope同跨度后匹配者勝。覆蓋相同字節(jié)區(qū)間的多個(gè)捕獲中查詢文件里靠后出現(xiàn)的 pattern 獲勝。因此應(yīng)當(dāng)把通用規(guī)則放在前面、需要覆蓋它的具體規(guī)則放在后面。嵌套節(jié)點(diǎn)最內(nèi)層者勝。當(dāng)父節(jié)點(diǎn)和子節(jié)點(diǎn)都覆蓋某段文本時(shí)無(wú)論文件順序如何子節(jié)點(diǎn)innermost的捕獲獲勝。規(guī)則 2 的一個(gè)常見(jiàn)后果捕獲你要捕獲的那個(gè)葉子節(jié)點(diǎn)。如果把function放在包裹調(diào)用的外層節(jié)點(diǎn)上它會(huì)輸給內(nèi)部 identifier 上的基礎(chǔ)規(guī)則(identifier) variable——所以應(yīng)當(dāng)把function直接放在被調(diào)用的標(biāo)識(shí)符節(jié)點(diǎn)本身。從源碼結(jié)構(gòu)可以印證這一最內(nèi)層獲勝的實(shí)現(xiàn)方式高亮器以作用域棧的形式工作捕獲進(jìn)入/離開節(jié)點(diǎn)時(shí)向棧上壓入/彈出 scope取棧頂即當(dāng)前字節(jié)的獲勝捕獲。helix-core/src/syntax.rs 中advance()返回HighlightEvent::Push/Refresh事件而測(cè)試工具中同樣按active棧的last()棧頂讀取獲勝捕獲見(jiàn) xtask/src/main.rs。語(yǔ)法無(wú)法區(qū)分時(shí)的啟發(fā)式大小寫匹配當(dāng)語(yǔ)法本身無(wú)法區(qū)分某個(gè) scope 時(shí)例如 C 中全大寫標(biāo)識(shí)符既可能是宏也可能是常量常用大小寫啟發(fā)式配合#match?謂詞過(guò)濾((identifier) constant (#match? constant ^[A-Z][A-Z_]*$))該謂詞只保留匹配正則^[A-Z][A-Z_]*$全大寫下劃線開頭的標(biāo)識(shí)符。#match?謂詞在倉(cāng)庫(kù)的查詢集中被廣泛使用例如 runtime/queries/bash/highlights.scm 即依賴此類謂詞區(qū)分變量與常量。測(cè)試與驗(yàn)證query-check 與 highlight-check對(duì)高亮查詢的驗(yàn)證分兩層分別對(duì)應(yīng)兩類錯(cuò)誤1.cargo xtask query-check [language]語(yǔ)法層校驗(yàn)確認(rèn)查詢對(duì)相應(yīng)語(yǔ)法是合法的節(jié)點(diǎn)名存在、捕獲名合規(guī)等。省略 language 參數(shù)時(shí)檢查全部語(yǔ)言。這一層抓不到優(yōu)先級(jí)錯(cuò)誤——查詢完全合法但捕獲選錯(cuò)的寫法它無(wú)法發(fā)現(xiàn)。2.cargo xtask highlight-check [language]真實(shí)高亮器回歸測(cè)試該任務(wù)運(yùn)行真正的高亮器對(duì)tests/query/highlights/language-id/name.ext下的語(yǔ)料文件做斷言。語(yǔ)料采用 nvim-treesitter 風(fēng)格的 caret 注釋行在代碼行下方寫注釋^字符的列位置對(duì)準(zhǔn)上一行的 token后跟期望的獲勝捕獲foo(bar) // ^ function // ^^^ variable每個(gè)^斷言其上方列位置處獲勝捕獲必須與capture完全一致期望名前的!表示取反斷言該列不是某個(gè)捕獲斷言行必須是注釋且首個(gè)^之前只有注釋引導(dǎo)符不含字母數(shù)字以避免把代碼里的^運(yùn)算符如a ^ b誤判為斷言行。倉(cāng)庫(kù)中已有大量此類語(yǔ)料例如 tests/query/highlights/rust/calls.rsfn main() { invokeit(); // ^ function let s String::new(); // ^ type }該文件斷言函數(shù)調(diào)用invokeit處獲勝捕獲是function而非基礎(chǔ)的variableString::new中的類型位置是type——恰好就是前文兩條優(yōu)先級(jí)規(guī)則的直接回歸用例。目前語(yǔ)料覆蓋 rust、cpp、go、python、typescript、tsx、javascript、bash 等數(shù)十種語(yǔ)言全部位于 tests/query/highlights/ 目錄。3.cargo xtask highlight-check --dump language file調(diào)試輔助對(duì)任意文件逐 span 打印獲勝捕獲用于編寫斷言時(shí)發(fā)現(xiàn)確切的capture名。輸出格式為scopeTAB文本跳過(guò)純空白 span實(shí)現(xiàn)見(jiàn) xtask/src/main.rs。從實(shí)現(xiàn)上補(bǔ)充兩點(diǎn)細(xì)節(jié)見(jiàn) xtask/src/main.rs該工具會(huì)掃描全部語(yǔ)言查詢文件中出現(xiàn)的捕獲名highlights.scm與locals.scm把每個(gè)捕獲名映射到它自己喂給高亮器從而直接讀回獲勝的capture原始名字無(wú)需手工維護(hù) scope 列表其中l(wèi)ocal.definition.*前綴的 locals 捕獲會(huì)被解析為引用實(shí)際應(yīng)用的高亮local.前綴名除外高亮失敗語(yǔ)法規(guī)格未構(gòu)建時(shí)corpus 模式會(huì)打印skipped并跳過(guò)而非 panic允許只構(gòu)建部分語(yǔ)法的開發(fā)環(huán)境運(yùn)行對(duì)應(yīng)語(yǔ)言的檢查。小結(jié)編寫高亮查詢的自檢清單結(jié)合手冊(cè)與倉(cāng)庫(kù)實(shí)踐編寫或修改highlights.scm時(shí)可按以下清單自檢文件位置正確runtime/queries/{language}/highlights.scm每個(gè)捕獲選了最具體的 scopefunction.methodvsfunction、variable.other.member完整清單參照 book/src/themes.md共享規(guī)則在前、覆蓋規(guī)則在后需要覆蓋嵌套節(jié)點(diǎn)時(shí)把捕獲放在葉子節(jié)點(diǎn)上使用; inherits:復(fù)用基礎(chǔ)語(yǔ)言查詢時(shí)確認(rèn)所有捕獲在每個(gè)繼承它的語(yǔ)法中都合法語(yǔ)法無(wú)法區(qū)分的 scope 用#match?謂詞如大小寫正則做啟發(fā)式過(guò)濾先跑cargo xtask query-check language驗(yàn)證合法性再為關(guān)鍵優(yōu)先級(jí)場(chǎng)景在tests/query/highlights/language-id/下添加 caret 斷言語(yǔ)料跑cargo xtask highlight-check language回歸驗(yàn)證遇到不確定的捕獲名用cargo xtask highlight-check --dump language file打印真實(shí)獲勝結(jié)果。這樣即可保證貢獻(xiàn)的高亮查詢既合法、又在真實(shí)高亮器中產(chǎn)生符合預(yù)期的著色結(jié)果?!久赓M(fèi)下載鏈接】helixA post-modern modal text editor.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/he/helix創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考