Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions crates/biome_markdown_syntax/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
biome_rowan = { workspace = true, features = ["serde"] }
schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }
biome_rowan = { workspace = true, features = ["serde"] }
biome_string_case = { workspace = true }
camino = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }

[features]
schema = ["biome_rowan/serde", "schemars"]
experimental-markdown = []
schema = ["biome_rowan/serde", "schemars"]

[lints]
workspace = true
66 changes: 66 additions & 0 deletions crates/biome_markdown_syntax/src/file_source.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use biome_rowan::FileSourceError;
use biome_string_case::StrLikeExtension;
use camino::Utf8Path;

#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(
Debug, Clone, Default, Copy, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize,
)]
enum MarkdownVariant {
#[default]
Standard,
}

#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(
Debug, Clone, Default, Copy, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize,
)]
pub struct MdFileSource {
variant: MarkdownVariant,
}

impl MdFileSource {
pub fn markdown() -> Self {
Self {
variant: MarkdownVariant::Standard,
}
}

/// Try to return the Markdown file source corresponding to this file name from well-known files
pub fn try_from_well_known(_: &Utf8Path) -> Result<Self, FileSourceError> {
Err(FileSourceError::UnknownFileName)
}

pub fn try_from_extension(extension: &str) -> Result<Self, FileSourceError> {
match extension {
#[cfg(feature = "experimental-markdown")]
"md" | "markdown" => Ok(Self::markdown()),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will enable handling markdown to our users, and we're not ready for this yet. To prevent this, do the same thing I did for SCSS in this PR: #9091

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

_ => Err(FileSourceError::UnknownExtension),
}
}

pub fn try_from_language_id(language_id: &str) -> Result<Self, FileSourceError> {
match language_id {
#[cfg(feature = "experimental-markdown")]
"markdown" => Ok(Self::markdown()),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here, let's gate it under a cargo feature

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

_ => Err(FileSourceError::UnknownLanguageId),
}
}
}

impl TryFrom<&Utf8Path> for MdFileSource {
type Error = FileSourceError;

fn try_from(path: &Utf8Path) -> Result<Self, Self::Error> {
if let Ok(file_source) = Self::try_from_well_known(path) {
return Ok(file_source);
}

let Some(extension) = path.extension() else {
return Err(FileSourceError::MissingFileExtension);
};
// We assume the file extensions are case-insensitive
// and we use the lowercase form of them for pattern matching
Self::try_from_extension(&extension.to_ascii_lowercase_cow())
}
}
3 changes: 3 additions & 0 deletions crates/biome_markdown_syntax/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#![deny(clippy::use_self)]

pub mod file_source;
#[macro_use]
mod generated;
mod syntax_node;

pub use file_source::MdFileSource;

pub use self::generated::*;
use biome_rowan::{RawSyntaxKind, SyntaxKind, TriviaPieceKind};
pub use syntax_node::*;
Expand Down
3 changes: 3 additions & 0 deletions crates/biome_service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ biome_json_analyze = { workspace = true }
biome_json_formatter = { workspace = true, features = ["serde"] }
biome_json_parser = { workspace = true }
biome_json_syntax = { workspace = true }
biome_markdown_parser = { workspace = true }
biome_markdown_syntax = { workspace = true }
biome_module_graph = { workspace = true, features = ["serde"] }
biome_package = { workspace = true }
biome_parser = { workspace = true }
Expand Down Expand Up @@ -99,6 +101,7 @@ schema = [
"biome_js_analyze/schema",
"biome_js_syntax/schema",
"biome_json_syntax/schema",
"biome_markdown_syntax/schema",
"biome_module_graph/schema",
"biome_text_edit/schema",
"dep:schemars",
Expand Down
235 changes: 235 additions & 0 deletions crates/biome_service/src/file_handlers/markdown.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
use super::{
Copy link
Copy Markdown
Contributor

@tidefield tidefield Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jfmcdowell . Sorry I'm afraid this will conflict with #9067 soon. :(

I was having this implementation in my local so I can verify the snapshots are correctly captured in the tests. I'm fixing some errors before pushing it to my PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI the reason I needed to implement what you had in this PR is because the tests need to have a TestFormatLanguage implementation which then requires ServiceLanguage implementation. So it overlaps with what you're doing in the PR.

Capabilities, DebugCapabilities, DocumentFileSource, EnabledForPath, ExtensionHandler,
FormatterCapabilities, ParseResult, ParserCapabilities, SearchCapabilities,
};
use crate::settings::{
FormatSettings, LanguageListSettings, LanguageSettings, OverrideSettings, ServiceLanguage,
Settings, SettingsWithEditor, check_feature_activity,
};
use crate::workspace::GetSyntaxTreeResult;
use biome_analyze::AnalyzerOptions;
use biome_configuration::analyzer::assist::AssistEnabled;
use biome_configuration::analyzer::linter::LinterEnabled;
use biome_configuration::formatter::FormatterEnabled;
use biome_formatter::{
IndentStyle, IndentWidth, LineEnding, LineWidth, SimpleFormatOptions, TrailingNewline,
};
use biome_fs::BiomePath;
use biome_markdown_parser::{MarkdownParseOptions, parse_markdown_with_cache};
use biome_markdown_syntax::{MarkdownLanguage, MarkdownSyntaxNode, MdDocument};
use biome_parser::{AnyParse, NodeParse};
use biome_rowan::NodeCache;
use camino::Utf8Path;

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct MarkdownFormatterSettings {
pub line_ending: Option<LineEnding>,
pub line_width: Option<LineWidth>,
pub indent_width: Option<IndentWidth>,
pub indent_style: Option<IndentStyle>,
pub trailing_newline: Option<TrailingNewline>,
pub enabled: Option<FormatterEnabled>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct MarkdownLinterSettings {
pub enabled: Option<LinterEnabled>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct MarkdownAssistSettings {
pub enabled: Option<AssistEnabled>,
}

impl ServiceLanguage for MarkdownLanguage {
type FormatterSettings = MarkdownFormatterSettings;
type LinterSettings = MarkdownLinterSettings;
type AssistSettings = MarkdownAssistSettings;
type FormatOptions = SimpleFormatOptions;
type ParserSettings = ();
type ParserOptions = MarkdownParseOptions;
type EnvironmentSettings = ();

fn lookup_settings(language: &LanguageListSettings) -> &LanguageSettings<Self> {
&language.markdown
}

fn resolve_environment(_settings: &Settings) -> Option<&Self::EnvironmentSettings> {
None
}

fn resolve_parse_options(
_overrides: &OverrideSettings,
_language: &Self::ParserSettings,
_path: &BiomePath,
_file_source: &DocumentFileSource,
) -> Self::ParserOptions {
MarkdownParseOptions::default()
}

fn resolve_format_options(
global: &FormatSettings,
overrides: &OverrideSettings,
language: &Self::FormatterSettings,
path: &BiomePath,
_document_file_source: &DocumentFileSource,
) -> Self::FormatOptions {
// TODO: apply markdown overrides once markdown override settings are introduced.
let _ = (overrides, path);

let indent_style = language
.indent_style
.or(global.indent_style)
.unwrap_or_default();
let line_width = language
.line_width
.or(global.line_width)
.unwrap_or_default();
let indent_width = language
.indent_width
.or(global.indent_width)
.unwrap_or_default();
let line_ending = language
.line_ending
.or(global.line_ending)
.unwrap_or_default();
let trailing_newline = language
.trailing_newline
.or(global.trailing_newline)
.unwrap_or_default();
SimpleFormatOptions {
indent_style,
indent_width,
line_width,
line_ending,
trailing_newline,
}
}

fn resolve_analyzer_options(
_global: &Settings,
_language: &Self::LinterSettings,
_environment: Option<&Self::EnvironmentSettings>,
path: &BiomePath,
_file_source: &DocumentFileSource,
suppression_reason: Option<&str>,
) -> AnalyzerOptions {
AnalyzerOptions::default()
.with_file_path(path.as_path())
.with_suppression_reason(suppression_reason)
}

fn linter_enabled_for_file_path(settings: &Settings, path: &Utf8Path) -> bool {
// TODO: evaluate markdown override patterns once markdown override settings are introduced.
let _ = path;

check_feature_activity(
settings.languages.markdown.linter.enabled,
settings.linter.enabled,
)
.unwrap_or_default()
.into()
}

fn formatter_enabled_for_file_path(settings: &Settings, path: &Utf8Path) -> bool {
// TODO: evaluate markdown override patterns once markdown override settings are introduced.
let _ = path;

check_feature_activity(
settings.languages.markdown.formatter.enabled,
settings.formatter.enabled,
)
.unwrap_or_default()
.into()
}

fn assist_enabled_for_file_path(settings: &Settings, path: &Utf8Path) -> bool {
// TODO: evaluate markdown override patterns once markdown override settings are introduced.
let _ = path;

check_feature_activity(
settings.languages.markdown.assist.enabled,
settings.assist.enabled,
)
.unwrap_or_default()
.into()
}
}

#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct MarkdownFileHandler;

impl ExtensionHandler for MarkdownFileHandler {
fn capabilities(&self) -> Capabilities {
Capabilities {
enabled_for_path: EnabledForPath {
formatter: Some(formatter_enabled),
linter: Some(linter_enabled),
assist: Some(assist_enabled),
search: None,
},
parser: ParserCapabilities {
parse: Some(parse),
parse_embedded_nodes: None,
},
debug: DebugCapabilities {
debug_syntax_tree: Some(debug_syntax_tree),
debug_control_flow: None,
debug_formatter_ir: None,
debug_type_info: None,
debug_registered_types: None,
debug_semantic_model: None,
},
analyzer: Default::default(),
formatter: FormatterCapabilities {
format: None,
format_range: None,
format_on_type: None,
format_embedded: None,
},
search: SearchCapabilities { search: None },
}
}
}

fn formatter_enabled(path: &Utf8Path, settings: &SettingsWithEditor) -> bool {
settings.formatter_enabled_for_file_path::<MarkdownLanguage>(path)
}

fn linter_enabled(path: &Utf8Path, settings: &SettingsWithEditor) -> bool {
settings.linter_enabled_for_file_path::<MarkdownLanguage>(path)
}

fn assist_enabled(path: &Utf8Path, settings: &SettingsWithEditor) -> bool {
settings.assist_enabled_for_file_path::<MarkdownLanguage>(path)
}

fn parse(
_biome_path: &BiomePath,
file_source: DocumentFileSource,
text: &str,
settings: &SettingsWithEditor,
cache: &mut NodeCache,
) -> ParseResult {
let options = settings.parse_options::<MarkdownLanguage>(_biome_path, &file_source);
let parse = parse_markdown_with_cache(text, cache, options);
let any_parse =
NodeParse::new(parse.syntax().as_send().unwrap(), parse.into_diagnostics()).into();

ParseResult {
any_parse,
language: Some(file_source),
}
}

fn debug_syntax_tree(_biome_path: &BiomePath, parse: AnyParse) -> GetSyntaxTreeResult {
let syntax: MarkdownSyntaxNode = parse.syntax();
let tree: MdDocument = parse.tree();
GetSyntaxTreeResult {
cst: format!("{syntax:#?}"),
ast: format!("{tree:#?}"),
}
}
Loading
Loading