Skip to content

Commit 5aeb343

Browse files
committed
support ---@language <lang> inject language to code block str
1 parent c5a3506 commit 5aeb343

File tree

16 files changed

+322
-5
lines changed

16 files changed

+322
-5
lines changed

crates/emmylua_ls/locales/tags/en.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,16 @@ tags.export: |
232232
233233
return export
234234
```
235+
tags.language: |
236+
The `language` tag is used to specify language injection for code blocks.
237+
Example:
238+
```lua
239+
---@language sql
240+
local t = [[
241+
SELECT * FROM users WHERE id = 1;
242+
SELECT name, email FROM users WHERE active = 1;
243+
UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = 1;
244+
DELETE FROM users WHERE id = 2;
245+
INSERT INTO users (name, email) VALUES ('Alice', '[email protected]');
246+
]]
247+
```

crates/emmylua_ls/locales/tags/zh_CN.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,4 +231,17 @@ tags.export: |
231231
end
232232
233233
return export
234+
```
235+
tags.language: |
236+
`language` 标签用于为代码块指定语言注入。
237+
示例:
238+
```lua
239+
---@language sql
240+
local t = [[
241+
SELECT * FROM users WHERE id = 1;
242+
SELECT name, email FROM users WHERE active = 1;
243+
UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = 1;
244+
DELETE FROM users WHERE id = 2;
245+
INSERT INTO users (name, email) VALUES ('Alice', '[email protected]');
246+
]]
234247
```

crates/emmylua_ls/locales/tags/zh_HK.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,17 @@ tags.export: |
229229
export.func = function()
230230
-- 在其他文件輸入`func`時會提示導入
231231
end
232+
```
233+
tags.language: |
234+
`language` 標籤用於為程式碼塊指定語言注入。
235+
示例:
236+
```lua
237+
---@language sql
238+
local t = [[
239+
SELECT * FROM users WHERE id = 1;
240+
SELECT name, email FROM users WHERE active = 1;
241+
UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = 1;
242+
DELETE FROM users WHERE id = 2;
243+
INSERT INTO users (name, email) VALUES ('Alice', '[email protected]');
244+
]]
232245
```

crates/emmylua_ls/src/handlers/completion/data/doc_tags.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ pub const DOC_TAGS: &[&str] = &[
3131
"readonly",
3232
"return_cast",
3333
"export",
34+
"language",
3435
];

crates/emmylua_ls/src/handlers/semantic_token/build_semantic_tokens.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
use super::{
22
SEMANTIC_TOKEN_MODIFIERS, SEMANTIC_TOKEN_TYPES, semantic_token_builder::SemanticBuilder,
33
};
4-
use crate::context::ClientId;
54
use crate::util::parse_desc;
5+
use crate::{context::ClientId, handlers::semantic_token::langauge_injector::inject_language};
66
use emmylua_code_analysis::{
77
Emmyrc, LuaDecl, LuaDeclExtra, LuaMemberId, LuaMemberOwner, LuaSemanticDeclId, LuaType,
88
LuaTypeDeclId, SemanticDeclLevel, SemanticModel, WorkspaceId, check_export_visibility,
99
parse_require_module_info,
1010
};
1111
use emmylua_parser::{
12-
LuaAst, LuaAstNode, LuaAstToken, LuaDocFieldKey, LuaDocObjectFieldKey, LuaExpr,
12+
LuaAst, LuaAstNode, LuaAstToken, LuaComment, LuaDocFieldKey, LuaDocObjectFieldKey, LuaExpr,
1313
LuaGeneralToken, LuaKind, LuaLiteralToken, LuaNameToken, LuaSyntaxKind, LuaSyntaxNode,
1414
LuaSyntaxToken, LuaTokenKind, LuaVarExpr,
1515
};
@@ -61,7 +61,9 @@ fn build_tokens_semantic_token(
6161
) {
6262
match token.kind().into() {
6363
LuaTokenKind::TkLongString | LuaTokenKind::TkString => {
64-
builder.push(token, SemanticTokenType::STRING);
64+
if !builder.is_lang_inject_range(&token.text_range()) {
65+
builder.push(token, SemanticTokenType::STRING);
66+
}
6567
}
6668
LuaTokenKind::TkAnd
6769
| LuaTokenKind::TkBreak
@@ -147,7 +149,8 @@ fn build_tokens_semantic_token(
147149
| LuaTokenKind::TkTagUsing
148150
| LuaTokenKind::TkTagSource
149151
| LuaTokenKind::TkTagReturnCast
150-
| LuaTokenKind::TkTagExport => {
152+
| LuaTokenKind::TkTagExport
153+
| LuaTokenKind::TkLanguage => {
151154
builder.push_with_modifier(
152155
token,
153156
SemanticTokenType::KEYWORD,
@@ -623,6 +626,14 @@ fn build_node_semantic_token(
623626
);
624627
render_desc_ranges(builder, text, items, desc_range);
625628
}
629+
LuaAst::LuaDocTagLanguage(language) => {
630+
let name = language.get_name_token()?;
631+
builder.push(name.syntax(), SemanticTokenType::STRING);
632+
let language_text = name.get_name_text();
633+
let comment = language.ancestors::<LuaComment>().next()?;
634+
635+
inject_language(builder, language_text, comment);
636+
}
626637
_ => {}
627638
}
628639

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
use emmylua_parser::{
2+
LexerState, LuaAstNode, LuaAstToken, LuaComment, LuaLiteralExpr, LuaLiteralToken,
3+
LuaStringToken, Reader, SourceRange,
4+
};
5+
use emmylua_parser_desc::{
6+
CodeBlockHighlightKind, CodeBlockLang, DescItem, DescItemKind, ResultContainer, process_code,
7+
};
8+
use lsp_types::SemanticTokenType;
9+
use rowan::{TextRange, TextSize};
10+
11+
use crate::handlers::semantic_token::semantic_token_builder::SemanticBuilder;
12+
13+
pub fn inject_language(
14+
builder: &mut SemanticBuilder,
15+
lang_name: &str,
16+
comment: LuaComment,
17+
) -> Option<()> {
18+
// Implementation for injecting the language
19+
let owner = comment.get_owner()?;
20+
let lang = CodeBlockLang::try_parse(lang_name)?;
21+
for literal in owner.descendants::<LuaLiteralExpr>() {
22+
if let Some(LuaLiteralToken::String(str_token)) = literal.get_literal() {
23+
let code_block_info = divide_into_quote_and_code_block(&str_token)?;
24+
let code_block_range = code_block_info.code_block;
25+
let code_block_source = SourceRange::from_start_end(
26+
u32::from(code_block_range.start()) as usize,
27+
u32::from(code_block_range.end()) as usize,
28+
);
29+
let code_block_str = str_token.slice(code_block_range)?;
30+
let reader = Reader::new(code_block_str);
31+
let mut result = InjectResult::new();
32+
process_code(
33+
&mut result,
34+
code_block_source,
35+
reader,
36+
LexerState::Normal,
37+
lang,
38+
);
39+
40+
for desc_item in result.results() {
41+
if let DescItemKind::CodeBlockHl(highlight_kind) = desc_item.kind {
42+
let token_type = match highlight_kind {
43+
CodeBlockHighlightKind::Keyword => SemanticTokenType::KEYWORD,
44+
CodeBlockHighlightKind::String => SemanticTokenType::STRING,
45+
CodeBlockHighlightKind::Number => SemanticTokenType::NUMBER,
46+
CodeBlockHighlightKind::Comment => SemanticTokenType::COMMENT,
47+
CodeBlockHighlightKind::Function => SemanticTokenType::FUNCTION,
48+
CodeBlockHighlightKind::Class => SemanticTokenType::CLASS,
49+
CodeBlockHighlightKind::Enum => SemanticTokenType::ENUM,
50+
CodeBlockHighlightKind::Variable => SemanticTokenType::VARIABLE,
51+
CodeBlockHighlightKind::Property => SemanticTokenType::PROPERTY,
52+
CodeBlockHighlightKind::Decorator => SemanticTokenType::DECORATOR,
53+
CodeBlockHighlightKind::Operators => SemanticTokenType::OPERATOR,
54+
_ => continue, // Fallback for other kinds
55+
};
56+
57+
let sub_token_range = TextRange::new(
58+
desc_item.range.start() + code_block_range.start(),
59+
desc_item.range.end() + code_block_range.start(),
60+
);
61+
if let Some(token_text) = str_token.slice(sub_token_range) {
62+
builder.push_at_range(token_text, sub_token_range, token_type, &[]);
63+
}
64+
}
65+
}
66+
67+
for quote_range in code_block_info.quote_ranges {
68+
let len = u32::from(quote_range.end() - quote_range.start());
69+
builder.push_at_position(quote_range.start(), len, SemanticTokenType::STRING, None);
70+
}
71+
builder.add_lang_inject_range(str_token.get_range());
72+
}
73+
}
74+
75+
Some(())
76+
}
77+
78+
struct InjectResult {
79+
result: Vec<DescItem>,
80+
}
81+
82+
impl ResultContainer for InjectResult {
83+
fn results(&self) -> &Vec<DescItem> {
84+
&self.result
85+
}
86+
87+
fn results_mut(&mut self) -> &mut Vec<DescItem> {
88+
&mut self.result
89+
}
90+
91+
fn cursor_position(&self) -> Option<usize> {
92+
None
93+
}
94+
}
95+
96+
impl InjectResult {
97+
pub fn new() -> Self {
98+
InjectResult { result: Vec::new() }
99+
}
100+
}
101+
102+
struct CodeBlockInfo {
103+
code_block: TextRange,
104+
quote_ranges: Vec<TextRange>,
105+
}
106+
107+
fn divide_into_quote_and_code_block(str_token: &LuaStringToken) -> Option<CodeBlockInfo> {
108+
let str_token_range = str_token.get_range();
109+
let text = str_token.get_text();
110+
let mut quote_ranges = Vec::new();
111+
112+
if text.is_empty() {
113+
return None;
114+
}
115+
116+
let mut code_block_start = str_token_range.start();
117+
let mut code_block_end = str_token_range.end();
118+
let range_start = str_token_range.start();
119+
if text.starts_with("[[") || text.starts_with("[=") {
120+
let mut equal_count = 0;
121+
let mut start_end = 2;
122+
123+
for c in text.chars().skip(1) {
124+
if c == '=' {
125+
equal_count += 1;
126+
} else if c == '[' {
127+
start_end = 2 + equal_count;
128+
break;
129+
} else {
130+
break;
131+
}
132+
}
133+
134+
let start_quote_range =
135+
TextRange::new(range_start, range_start + TextSize::from(start_end as u32));
136+
quote_ranges.push(start_quote_range);
137+
code_block_start = start_quote_range.end();
138+
139+
let end_pattern = format!("]{}]", "=".repeat(equal_count));
140+
if let Some(end_pos) = text.rfind(&end_pattern) {
141+
let end_quote_start = range_start + rowan::TextSize::from(end_pos as u32);
142+
let end_quote_range = TextRange::new(
143+
end_quote_start,
144+
range_start + rowan::TextSize::from(text.len() as u32),
145+
);
146+
quote_ranges.push(end_quote_range);
147+
code_block_end = end_quote_range.start();
148+
}
149+
} else if text.starts_with('"') || text.starts_with('\'') {
150+
let quote_char = text.chars().next().unwrap();
151+
let start_quote_range = TextRange::new(range_start, range_start + TextSize::from(1));
152+
quote_ranges.push(start_quote_range);
153+
code_block_start = start_quote_range.end();
154+
155+
if text.len() > 1 && text.ends_with(quote_char) {
156+
let end_quote_start = range_start + TextSize::from((text.len() - 1) as u32);
157+
let end_quote_range = TextRange::new(
158+
end_quote_start,
159+
range_start + TextSize::from(text.len() as u32),
160+
);
161+
quote_ranges.push(end_quote_range);
162+
code_block_end = end_quote_range.start();
163+
}
164+
}
165+
166+
if code_block_start > code_block_end {
167+
return None;
168+
}
169+
170+
Some(CodeBlockInfo {
171+
code_block: TextRange::new(code_block_start, code_block_end),
172+
quote_ranges,
173+
})
174+
}

crates/emmylua_ls/src/handlers/semantic_token/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod build_semantic_tokens;
2+
mod langauge_injector;
23
mod semantic_token_builder;
34

45
use crate::context::{ClientId, ServerContextSnapshot};

crates/emmylua_ls/src/handlers/semantic_token/semantic_token_builder.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ use emmylua_code_analysis::LuaDocument;
22
use emmylua_parser::LuaSyntaxToken;
33
use lsp_types::{SemanticToken, SemanticTokenModifier, SemanticTokenType};
44
use rowan::{TextRange, TextSize};
5-
use std::{collections::HashMap, vec::Vec};
5+
use std::{
6+
collections::{HashMap, HashSet},
7+
vec::Vec,
8+
};
69

710
pub const SEMANTIC_TOKEN_TYPES: &[SemanticTokenType] = &[
811
SemanticTokenType::NAMESPACE,
@@ -65,6 +68,7 @@ pub struct SemanticBuilder<'a> {
6568
type_to_id: HashMap<SemanticTokenType, u32>,
6669
modifier_to_id: HashMap<SemanticTokenModifier, u32>,
6770
data: HashMap<TextSize, SemanticTokenData>,
71+
lang_inject_range: HashSet<TextRange>,
6872
}
6973

7074
impl<'a> SemanticBuilder<'a> {
@@ -89,6 +93,7 @@ impl<'a> SemanticBuilder<'a> {
8993
type_to_id,
9094
modifier_to_id,
9195
data: HashMap::new(),
96+
lang_inject_range: HashSet::new(),
9297
}
9398
}
9499

@@ -279,4 +284,12 @@ impl<'a> SemanticBuilder<'a> {
279284

280285
result
281286
}
287+
288+
pub fn add_lang_inject_range(&mut self, range: TextRange) {
289+
self.lang_inject_range.insert(range);
290+
}
291+
292+
pub fn is_lang_inject_range(&self, range: &TextRange) -> bool {
293+
self.lang_inject_range.contains(range)
294+
}
282295
}

crates/emmylua_parser/src/grammar/doc/tag.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ fn parse_tag_detail(p: &mut LuaDocParser) -> ParseResult {
5656
LuaTokenKind::TkTagUsing => parse_tag_using(p),
5757
LuaTokenKind::TkTagMeta => parse_tag_meta(p),
5858
LuaTokenKind::TkTagExport => parse_tag_export(p),
59+
LuaTokenKind::TkLanguage => parse_tag_language(p),
5960

6061
// simple tag
6162
LuaTokenKind::TkTagVisibility => parse_tag_simple(p, LuaSyntaxKind::DocTagVisibility),
@@ -628,3 +629,14 @@ fn parse_tag_export(p: &mut LuaDocParser) -> ParseResult {
628629
parse_description(p);
629630
Ok(m.complete(p))
630631
}
632+
633+
fn parse_tag_language(p: &mut LuaDocParser) -> ParseResult {
634+
p.set_state(LuaDocLexerState::Normal);
635+
let m = p.mark(LuaSyntaxKind::DocTagLanguage);
636+
p.bump();
637+
expect_token(p, LuaTokenKind::TkName)?;
638+
639+
p.set_state(LuaDocLexerState::Description);
640+
parse_description(p);
641+
Ok(m.complete(p))
642+
}

crates/emmylua_parser/src/kind/lua_syntax_kind.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ pub enum LuaSyntaxKind {
9191
DocTagReadonly,
9292
DocTagReturnCast,
9393
DocTagExport,
94+
DocTagLanguage,
9495

9596
// doc Type
9697
TypeArray, // baseType []

0 commit comments

Comments
 (0)