Skip to content

Highlighting (e.g. in error reports) #134

@simonbuchan

Description

@simonbuchan

I got bugged by the lack of error report highlighting and dug into this a bit.

First I got highlighting going with my own diagnostics from mapping into a config shape, which meant digging into the highlighting support in miette

syntect which is the default highlighting used by miette requires a .sublime-syntax source to define the grammar, so I translated by hand the .tmLanguage.json in the vscode extension (since as far as I could google you need Sublime Text 3 to automatically translate), feel free to use if it's handy (I had some issue with includes so I unwrapped those, but it looks like it should work from the source?)

kdl.sublime-syntax
version: 2
name: kdl
file_extensions: [ kdl ]

scope: source.kdl

contexts:
  main:
    - scope: "invalid.illegal.kdl.bad-ident"
      match: "(?<!#)(?:true|false|null|nan|[-]?inf)"
    - scope: "constant.language.null.kdl"
      match: "#null"
    - scope: "constant.language.boolean.kdl"
      match: "#true|#false"
    - scope: "constant.language.other.kdl"
      match: "#nan|#inf|#-inf"
    - comment: "Floating point literal (fraction)"
      scope: "constant.numeric.float.rust"
      match: "\\b([0-9\\-\\+]|\\-|\\+)[0-9_]*\\.[0-9][0-9_]*([eE][+-]?[0-9_]+)?\\b"
    - comment: "Floating point literal (exponent)"
      scope: "constant.numeric.float.rust"
      match: "\\b[0-9][0-9_]*(\\.[0-9][0-9_]*)?[eE][+-]?[0-9_]+\\b"
    - comment: "Integer literal (decimal)"
      scope: "constant.numeric.integer.decimal.rust"
      match: "\\b[0-9\\-\\+][0-9_]*\\b"
    - comment": "Integer literal (hexadecimal)"
      scope: "constant.numeric.integer.hexadecimal.rust"
      match: "\\b0x[a-fA-F0-9][a-fA-F0-9_]*\\b"
    - comment: "Integer literal (octal)"
      scope: "constant.numeric.integer.octal.rust"
      match: "\\b0o[0-7][0-7_]*\\b"
    - comment: "Integer literal (binary)"
      scope: "constant.numeric.integer.binary.rust"
      match: "\\b0b[01][01_]*\\b"
    - embed: "string.quoted.other.raw.kdl"
      match: "(#+)(\"\"\"|\")"
      captures:
        0: punctuation.definition.string.begin.kdl
      escape: "\\2\\1"
      escape_captures:
        0: punctuation.definition.string.end.kdl
    - match: '"""'
      push: string_multi_line
    - match: '"'
      push: string_single_line

    - match: "/\\*"
      push: block_comment
    - match: "/\\*[\\*!](?![\\*/])"
      push: block_doc_comment

    - comment: "Slashdash block comment"
      match: "/-\\s*{"
      push: slashdash_block_comment
    - comment: "Slashdash inline comment"
      match: "(?<!^)\\s*/-\\s*"
      push: slashdash_comment
    - comment: "Slashdash node comment"
      match: "(?<=^)\\s*/-[^{]+$"
      push: slashdash_node_comment
    - comment: "Slashdash node comment"
      match: "(?<=^)\\s*/-[^{]+{"
      push: slashdash_node_with_children_comment

    - comment: "Single-line comment"
      match: "//"
      push: line_comment
    - scope: "entity.other.attribute-name.kdl"
      match: "(?![/\\\\{\\}#;\\[\\]\\=])[<>:\\w\\-_~,'`!\\?@\\$%^&*+|.\\(\\)]+\\d*[<>:\\w\\-_~,'`!\\?@\\$%^&*+|.\\(\\)]*(=)"
      captures:
        1: "punctuation.separator.key-value.kdl"
    - scope: "entity.name.tag.kdl"
      match: "((?<={|;)|^)\\s*(?![/\\\\{\\}#;\\[\\]\\=])[<>:\\w\\-_~,'`!\\?@\\$%^&*+|.\\(\\)]+\\d*[<>:\\w\\-_~,'`!\\?@\\$%^&*+|.\\(\\)]*"
    - scope: "string.unquoted.kdl"
      match: "(?![/\\\\{\\}#;\\[\\]\\=])[<>:\\w\\-_~,'`!\\?@\\$%^&*+|.\\(\\)]+\\d*[<>:\\w\\-_~,'`!\\?@\\$%^&*+|.\\(\\)]*"

  string_multi_line:
    - meta_scope: "string.quoted.triple.kdl"
    - scope: "constant.character.escape.kdl"
      match: "\\\\(:?[nrtbfs\\\\\"]|u\\{[a-fA-F0-9]{1,6}\\})"
    - match: '"""'
      pop: true
  string_single_line:
    - meta_scope: "string.quoted.double.kdl"
    - scope: "constant.character.escape.kdl"
      match: "\\\\(:?[nrtbfs\\\\\"]|u\\{[a-fA-F0-9]{1,6}\\})"
    - match: '"'
      pop: true
  block_comment:
    - comment: "Block comment"
      meta_scope: "comment.block.kdl"
    # syntect doesn't support - include: block_comment?
    - match: "/\\*[\\*!](?![\\*/])"
      push: block_doc_comment
    - match: "/\\*"
      push: block_comment
    - match: "\\*/"
      pop: true
  block_doc_comment:
    - comment: "Block documentation comment"
      meta_scope: "comment.block.documentation.kdl"
    - match: "/\\*[\\*!](?![\\*/])"
      push: block_doc_comment
    - match: "/\\*"
      push: block_comment
    - match: "\\*/"
      pop: true
  slashdash_block_comment:
    - meta_scope: "comment.block.slashdash.kdl"
      match: "\\}"
      pop: true
  slashdash_comment:
    - meta_scope: "comment.block.slashdash.kdl"
    - match: "\\s"
      pop: true
  slashdash_node_comment:
    - meta_scope: "comment.block.slashdash.kdl"
    - match: "(?:;|(?<!\\\\)$)"
      pop: true
  slashdash_node_with_children_comment:
    - meta_scope: "comment.block.slashdash.kdl"
    - match: "\\}"
      pop: true
  line_comment:
    - meta_scope: "comment.line.double-slash.kdl"
    - match: "$"
      pop: true

Then you can register this in miette with some horrible code like this (in the application):

fn main() -> miette::Result<()> {
  miette::set_hook(Box::new(miette_hook))?;

  ...
}

fn highlight_theme() -> syntect::highlighting::Theme {
    syntect::highlighting::ThemeSet::load_defaults()
        .themes
        .remove("Solarized (dark)")
        .expect("Couldn't load theme")
}

fn parse_kdl_syntax() -> syntect::parsing::SyntaxDefinition {
    syntect::parsing::SyntaxDefinition::load_from_str(
        include_str!("kdl.sublime-syntax"),
        false,
        None,
    )
    .expect("Failed to load KDL syntax definition")
}

fn miette_hook(_: &(dyn miette::Diagnostic + 'static)) -> Box<dyn miette::ReportHandler> {
    let mut syntax_set = syntect::parsing::SyntaxSetBuilder::new();
    syntax_set.add(parse_kdl_syntax());
    let syntax_set = syntax_set.build();
    let theme = highlight_theme();
    let highlighter = miette::highlighters::SyntectHighlighter::new(syntax_set, theme, false);

    Box::new(
        miette::MietteHandlerOpts::new()
            .with_syntax_highlighting(highlighter)
            .build(),
    )
}

Finally, you attach a SourceFile that declares this is a KDL file, e.g. with NamedSource :

match Config::parse(text) {
  Err(error) => {
      let report = miette::Report::new(error)
          .with_source_code(miette::NamedSource::new(path, text).with_language("kdl"));
      eprintln!("{report:?}");
      std::process::exit(1);
  }
  Ok(config) => ...
}

(Though I'm sure there's better options - this is the simplest I found that doesn't need bubbling miette::Report to main)

Unfortunately, this doesn't work with the parse errors issued by kdl as both the KdlError and each KdlDiagnostic include a #[source_code] source_code: Arc<String> which trumps any outer #[source_code].

I'm not quite sure what the right option to fix that is - maybe just switching to:

struct KdlSource(Arc<String>);

impl miette::SourceCode for KdlSource {
  fn read_span<'a>(
    &'a self,
    span: &SourceSpan,
    context_lines_before: usize,
    context_lines_after: usize,
  ) -> Result<Box<dyn SpanContents<'a> + 'a>, MietteError> {
    let contents = (**self).read_span(span, context_lines_before, context_lines_after)?;
    Ok(contents.with_language("kdl"))
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions