feat(lsp): semantic tokens for headline keyword, priority and timestamp

This commit is contained in:
PoiScript 2024-01-10 05:33:05 +08:00
parent e0021b4a91
commit edd73e3c6d
No known key found for this signature in database
GPG key ID: 22C2B1249D99985E
4 changed files with 87 additions and 84 deletions

View file

@ -75,6 +75,29 @@
}
}
],
"semanticTokenScopes": [
{
"language": "org",
"scopes": {
"headlineTodoKeyword": [
"invalid.illegal.org"
],
"headlineDoneKeyword": [
"keyword.control.org"
],
"headlineTags": [
"markup.italic.org",
"keyword.control.org"
],
"headlinePriority": [
"keyword.control.org"
],
"timestamp": [
"variable.org"
]
}
}
],
"grammars": [
{
"language": "org",

View file

@ -1863,9 +1863,6 @@
},
"common": {
"patterns": [
{
"include": "#timestamp"
},
{
"include": "#link"
},
@ -1899,23 +1896,6 @@
"patterns": [
{
"include": "#common"
},
{
"include": "#todo"
},
{
"include": "#done"
},
{
"include": "#userKeywords"
}
]
},
"timestamp": {
"patterns": [
{
"name": "variable.org",
"match": "\\[\\d{4}-\\d{1,2}-\\d{1,2}(?: \\w{3})?\\]"
}
]
},
@ -1953,30 +1933,6 @@
}
]
},
"todo": {
"patterns": [
{
"name": "invalid.illegal.org",
"match": "\\bTODO\\b"
}
]
},
"done": {
"patterns": [
{
"name": "keyword.control.org",
"match": "\\bDONE\\b"
}
]
},
"userKeywords": {
"patterns": [
{
"name": "string.quoted.double.org",
"match": "\\b([A-Z]{3,})\\b"
}
]
},
"bold": {
"patterns": [
{

View file

@ -64,8 +64,8 @@ impl LanguageServer for Backend {
semantic_tokens_options: SemanticTokensOptions {
work_done_progress_options: WorkDoneProgressOptions::default(),
legend: SemanticTokensLegend {
token_types: LEGEND_TYPE.into(),
token_modifiers: vec![],
token_types: semantic_token::TYPES.into(),
token_modifiers: semantic_token::MODIFIERS.into(),
},
range: Some(true),
full: Some(SemanticTokensFullOptions::Bool(true)),

View file

@ -1,13 +1,27 @@
use orgize::{
export::{Container, Event, TraversalContext, Traverser},
rowan::{ast::AstNode, TextRange},
SyntaxKind,
};
use tower_lsp::lsp_types::{Range, SemanticToken, SemanticTokenType};
use tower_lsp::lsp_types::{Range, SemanticToken, SemanticTokenModifier, SemanticTokenType};
use crate::org_document::OrgDocument;
/// Semantic token types that are used for highlighting
pub const LEGEND_TYPE: &[SemanticTokenType] = &[SemanticTokenType::COMMENT];
const TIMESTAMP: SemanticTokenType = SemanticTokenType::new("timestamp");
const HEADLINE_TODO_KEYWORD: SemanticTokenType = SemanticTokenType::new("headlineTodoKeyword");
const HEADLINE_DONE_KEYWORD: SemanticTokenType = SemanticTokenType::new("headlineDoneKeyword");
const HEADLINE_PRIORITY: SemanticTokenType = SemanticTokenType::new("headlinePriority");
const HEADLINE_TAGS: SemanticTokenType = SemanticTokenType::new("headlineTags");
pub const TYPES: &[SemanticTokenType] = &[
TIMESTAMP,
HEADLINE_TODO_KEYWORD,
HEADLINE_DONE_KEYWORD,
HEADLINE_PRIORITY,
HEADLINE_TAGS,
];
pub const MODIFIERS: &[SemanticTokenModifier] = &[];
pub struct SemanticTokenTraverser<'a> {
pub doc: &'a OrgDocument,
@ -21,37 +35,54 @@ pub struct SemanticTokenTraverser<'a> {
impl<'a> Traverser for SemanticTokenTraverser<'a> {
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
macro_rules! m {
($range:expr, $ty:expr $(,$modifiers:expr)*) => {{
if let Some(token) =
self.create_token($range.start().into(), $range.end().into(), $ty)
{
self.tokens.push(token);
}
}};
}
macro_rules! s {
($range:expr) => {
if let Some(range) = self.range {
if !range.contains_range($range) {
return ctx.skip();
}
}
};
}
match event {
Event::Enter(Container::Comment(comment)) => {
let range = comment.syntax().text_range();
Event::Enter(Container::Section(section)) => s!(section.syntax().text_range()),
Event::Enter(Container::Paragraph(paragraph)) => s!(paragraph.syntax().text_range()),
Event::Enter(Container::OrgTable(table)) => s!(table.syntax().text_range()),
Event::Enter(Container::List(list)) => s!(list.syntax().text_range()),
Event::Enter(Container::Drawer(drawer)) => s!(drawer.syntax().text_range()),
Event::Enter(Container::DynBlock(block)) => s!(block.syntax().text_range()),
if self.contains_range(range) {
if let Some(token) = self.create_token(
comment.begin(),
comment.end(),
SemanticTokenType::COMMENT,
) {
self.tokens.push(token);
Event::Enter(Container::Headline(headline)) => {
s!(headline.syntax().text_range());
for ch in headline.syntax().children_with_tokens() {
match ch.kind() {
SyntaxKind::HEADLINE_KEYWORD_DONE => {
m!(ch.text_range(), HEADLINE_DONE_KEYWORD)
}
SyntaxKind::HEADLINE_KEYWORD_TODO => {
m!(ch.text_range(), HEADLINE_TODO_KEYWORD)
}
SyntaxKind::HEADLINE_TAGS => m!(ch.text_range(), HEADLINE_TAGS),
SyntaxKind::HEADLINE_PRIORITY => m!(ch.text_range(), HEADLINE_PRIORITY),
SyntaxKind::NEW_LINE => break,
_ => {}
}
}
ctx.skip();
}
Event::Enter(Container::CommentBlock(comment)) => {
let range = comment.syntax().text_range();
if self.contains_range(range) {
if let Some(token) = self.create_token(
comment.begin(),
comment.end(),
SemanticTokenType::COMMENT,
) {
self.tokens.push(token);
}
}
ctx.skip();
}
Event::Timestamp(timestamp) => m!(timestamp.syntax().text_range(), TIMESTAMP),
_ => {}
}
@ -82,13 +113,6 @@ impl<'a> SemanticTokenTraverser<'a> {
}
}
fn contains_range(&self, range: TextRange) -> bool {
match self.range {
Some(r) => r.contains_range(range),
None => true,
}
}
fn create_token(
&mut self,
start: u32,
@ -96,11 +120,11 @@ impl<'a> SemanticTokenTraverser<'a> {
kind: SemanticTokenType,
) -> Option<SemanticToken> {
let length = end - start;
let token_type = LEGEND_TYPE.iter().position(|item| item == &kind)? as u32;
let token_type = TYPES.iter().position(|item| item == &kind)? as u32;
let line = self.doc.line_of(start);
let first = self.doc.line_of(line);
let start = self.doc.line_of(start) - first;
let start = start - self.doc.line_starts[line as usize];
let delta_line = line - self.previous_line;
let delta_start = if delta_line == 0 {