@@ -14,10 +14,10 @@ struct DiagnosticJson {
1414}
1515
1616fn compile(input_path: &str, dev: bool, error_overlay: bool, check_only: bool, output_path: Option<&str>, format_json: bool, json_collector: Option<&Mutex<Vec<DiagnosticJson>>>) -> (bool, Vec<PathBuf>) {
17− compile_inner(input_path, dev, error_overlay, check_only, output_path, format_json, json_collector, false, false, false)
17+ compile_inner(input_path, dev, error_overlay, check_only, output_path, format_json, json_collector, false, false, false, false)
1818}
1919
20−fn compile_inner(input_path: &str, dev: bool, error_overlay: bool, check_only: bool, output_path: Option<&str>, format_json: bool, json_collector: Option<&Mutex<Vec<DiagnosticJson>>>, minify: bool, compat: bool, strict: bool) -> (bool, Vec<PathBuf>) {
20+fn compile_inner(input_path: &str, dev: bool, error_overlay: bool, check_only: bool, output_path: Option<&str>, format_json: bool, json_collector: Option<&Mutex<Vec<DiagnosticJson>>>, minify: bool, compat: bool, strict: bool, partial: bool) -> (bool, Vec<PathBuf>) {
2121 let input = match fs::read_to_string(input_path) {
2222 Ok(s) => s,
2323 Err(e) => {
@@ -87,13 +87,21 @@ fn compile_inner(input_path: &str, dev: bool, error_overlay: bool, check_only: b
8787 let _ = fs::write(&out_path, &error_html);
8888 }
8989 } else {
90− let html = match (minify, dev, compat) {
91− (true, _, true) => htmlang::codegen::generate_minified_compat(&result.document),
92− (true, _, false) => htmlang::codegen::generate_minified(&result.document),
93− (false, true, true) => htmlang::codegen::generate_dev_compat(&result.document),
94− (false, true, false) => htmlang::codegen::generate_dev(&result.document),
95− (false, false, true) => htmlang::codegen::generate_compat(&result.document),
96− (false, false, false) => htmlang::codegen::generate(&result.document),
90+ let html = if partial {
91+ if dev {
92+ htmlang::codegen::generate_partial_dev(&result.document)
93+ } else {
94+ htmlang::codegen::generate_partial(&result.document)
95+ }
96+ } else {
97+ match (minify, dev, compat) {
98+ (true, _, true) => htmlang::codegen::generate_minified_compat(&result.document),
99+ (true, _, false) => htmlang::codegen::generate_minified(&result.document),
100+ (false, true, true) => htmlang::codegen::generate_dev_compat(&result.document),
101+ (false, true, false) => htmlang::codegen::generate_dev(&result.document),
102+ (false, false, true) => htmlang::codegen::generate_compat(&result.document),
103+ (false, false, false) => htmlang::codegen::generate(&result.document),
104+ }
97105 };
98106 match fs::write(&out_path, &html) {
99107 Ok(()) => eprintln!("wrote {}", out_path.display()),
@@ -356,6 +364,8 @@ Commands:
356364 bundle <file|dir> [-o <out>] Compile and inline all assets as data URIs
357365 size <file|dir> Report output sizes (raw, minified, ~gzip)
358366 benchmark <file|dir> Measure compile time and output size
367+ lsp Start the Language Server Protocol server
368+ completions <shell> Generate shell completions (bash, zsh, fish)
359369
360370Options:
361371 -w, --watch Watch for changes and recompile
@@ -367,6 +377,7 @@ Options:
367377 -c, --check Check for errors without writing output
368378 --compat Add vendor prefixes for broader browser support
369379 --strict Treat warnings as errors (useful for CI)
380+ --partial Output HTML fragment without document wrapper
370381 --format json Output diagnostics as JSON to stdout
371382 -h, --help Show this help
372383 -V, --version Show version
@@ -889,10 +900,24 @@ fn main() {
889900 let mut compat = false;
890901 let mut strict = false;
891902 let mut open_browser = false;
903+ let mut partial = false;
892904 let mut port: u16 = 3000;
893905 let mut output_path: Option<String> = None;
894906 let mut input_path = None;
895907
908+ // Handle "lsp" subcommand — launch the LSP server from the main binary
909+ if args.len() >= 2 && args[1] == "lsp" {
910+ run_lsp();
911+ return;
912+ }
913+
914+ // Handle "completions" subcommand — generate shell completions
915+ if args.len() >= 2 && args[1] == "completions" {
916+ let shell = args.get(2).map(|s| s.as_str()).unwrap_or("bash");
917+ print_shell_completions(shell);
918+ return;
919+ }
920+
896921 // Handle "init" subcommand
897922 if args.len() >= 2 && args[1] == "init" {
898923 let mut init_dir = ".";
@@ -1009,7 +1034,7 @@ fn main() {
10091034 }
10101035 let path_str = file.to_string_lossy().to_string();
10111036 let (has_errors, _) = if build_minify || build_compat {
1012− compile_inner(&path_str, false, false, false, effective_out.as_deref(), false, None, build_minify, build_compat, false)
1037+ compile_inner(&path_str, false, false, false, effective_out.as_deref(), false, None, build_minify, build_compat, false, false)
10131038 } else {
10141039 compile(&path_str, false, false, false, effective_out.as_deref(), false, None)
10151040 };
@@ -2919,6 +2944,7 @@ compile();
29192944 "--compat" => compat = true,
29202945 "--strict" => strict = true,
29212946 "--open" => open_browser = true,
2947+ "--partial" => partial = true,
29222948 "--format" => {
29232949 i += 1;
29242950 match args.get(i) {
@@ -3008,7 +3034,7 @@ compile();
30083034 }
30093035 out_p.to_string_lossy().to_string()
30103036 });
3011− let (has_errors, included) = compile_inner(&path_str, dev, serve, check, effective_out.as_deref(), format_json, json_collector.as_ref(), false, compat, strict);
3037+ let (has_errors, included) = compile_inner(&path_str, dev, serve, check, effective_out.as_deref(), format_json, json_collector.as_ref(), false, compat, strict, partial);
30123038 if has_errors {
30133039 any_errors = true;
30143040 }
@@ -3028,14 +3054,14 @@ compile();
30283054 return;
30293055 }
30303056
3031− // For directory serve mode, serve the directory with index.html
3057+ // For directory serve mode, serve the directory with route mapping
30323058 let reload_tx = if serve {
30333059 let (tx, _) = tokio::sync::broadcast::channel::<()>(16);
3034− let index_path = dir.join("index.html");
3060+ let serve_dir = dir.to_path_buf();
30353061 let server_tx = tx.clone();
30363062 std::thread::spawn(move || {
30373063 let rt = tokio::runtime::Runtime::new().expect("failed to create runtime");
3038− rt.block_on(htmlang::serve::run(port, index_path, server_tx));
3064+ rt.block_on(htmlang::serve::run_dir(port, serve_dir, server_tx));
30393065 });
30403066 if open_browser {
30413067 open_in_browser(port);
@@ -3051,7 +3077,7 @@ compile();
30513077
30523078 // --- Single file mode ---
30533079 let json_collector_single = if format_json { Some(Mutex::new(Vec::new())) } else { None };
3054− let (has_errors, included_files) = compile_inner(&input_path, dev, serve, check, output_path.as_deref(), format_json, json_collector_single.as_ref(), false, compat, strict);
3080+ let (has_errors, included_files) = compile_inner(&input_path, dev, serve, check, output_path.as_deref(), format_json, json_collector_single.as_ref(), false, compat, strict, partial);
30553081 if format_json {
30563082 if let Some(ref collector) = json_collector_single {
30573083 print_json_diagnostics(&collector.lock().unwrap());
@@ -3096,6 +3122,235 @@ compile();
30963122 );
30973123}
30983124
3125+// ---------------------------------------------------------------------------
3126+// LSP subcommand: launch the Language Server Protocol server
3127+// ---------------------------------------------------------------------------
3128+
3129+fn run_lsp() {
3130+ // Try to find the htmlang-lsp binary alongside this binary, or in PATH
3131+ let self_exe = env::current_exe().ok();
3132+ let lsp_name = if cfg!(windows) { "htmlang-lsp.exe" } else { "htmlang-lsp" };
3133+
3134+ let lsp_path = self_exe
3135+ .as_ref()
3136+ .and_then(|exe| exe.parent())
3137+ .map(|dir| dir.join(lsp_name))
3138+ .filter(|p| p.exists());
3139+
3140+ let lsp_cmd = lsp_path
3141+ .map(|p| p.to_string_lossy().to_string())
3142+ .unwrap_or_else(|| lsp_name.to_string());
3143+
3144+ let status = std::process::Command::new(&lsp_cmd)
3145+ .stdin(std::process::Stdio::inherit())
3146+ .stdout(std::process::Stdio::inherit())
3147+ .stderr(std::process::Stdio::inherit())
3148+ .status();
3149+
3150+ match status {
3151+ Ok(s) => {
3152+ if !s.success() {
3153+ process::exit(s.code().unwrap_or(1));
3154+ }
3155+ }
3156+ Err(_) => {
3157+ eprintln!("error: could not find htmlang-lsp binary");
3158+ eprintln!("hint: ensure htmlang-lsp is in the same directory as htmlang, or in your PATH");
3159+ process::exit(1);
3160+ }
3161+ }
3162+}
3163+
3164+// ---------------------------------------------------------------------------
3165+// Shell completion generation
3166+// ---------------------------------------------------------------------------
3167+
3168+fn print_shell_completions(shell: &str) {
3169+ match shell {
3170+ "bash" => print_bash_completions(),
3171+ "zsh" => print_zsh_completions(),
3172+ "fish" => print_fish_completions(),
3173+ _ => {
3174+ eprintln!("unknown shell: {}. Supported: bash, zsh, fish", shell);
3175+ process::exit(1);
3176+ }
3177+ }
3178+}
3179+
3180+fn print_bash_completions() {
3181+ println!(r#"_htmlang() {{
3182+ local cur prev commands
3183+ cur="${{COMP_WORDS[COMP_CWORD]}}"
3184+ prev="${{COMP_WORDS[COMP_CWORD-1]}}"
3185+ commands="init new build serve watch check convert fmt sitemap lint stats preview diff export repl feed components deps dead-code deploy playground clean upgrade create-component outline doctor migrate bundle size benchmark test lsp completions"
3186+
3187+ if [ "$COMP_CWORD" -eq 1 ]; then
3188+ COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
3189+ COMPREPLY+=( $(compgen -f -X '!*.hl' -- "$cur") )
3190+ COMPREPLY+=( $(compgen -d -- "$cur") )
3191+ return
3192+ fi
3193+
3194+ case "$prev" in
3195+ -o|--output) COMPREPLY=( $(compgen -f -- "$cur") ) ;;
3196+ -p|--port) COMPREPLY=() ;;
3197+ --format) COMPREPLY=( $(compgen -W "json" -- "$cur") ) ;;
3198+ --template|-t) COMPREPLY=( $(compgen -W "blog docs portfolio" -- "$cur") ) ;;
3199+ --provider) COMPREPLY=( $(compgen -W "github-pages netlify vercel cloudflare" -- "$cur") ) ;;
3200+ completions) COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") ) ;;
3201+ *)
3202+ COMPREPLY=( $(compgen -W "-w --watch -s --serve -d --dev -c --check --compat --strict --partial --open --format --minify -o --output -p --port -h --help -V --version" -- "$cur") )
3203+ COMPREPLY+=( $(compgen -f -X '!*.hl' -- "$cur") )
3204+ COMPREPLY+=( $(compgen -d -- "$cur") )
3205+ ;;
3206+ esac
3207+}}
3208+complete -F _htmlang htmlang"#);
3209+}
3210+
3211+fn print_zsh_completions() {
3212+ println!(r#"#compdef htmlang
3213+
3214+_htmlang() {{
3215+ local -a commands=(
3216+ 'init:Create a new project'
3217+ 'new:Create a new .hl page'
3218+ 'build:Compile all .hl files'
3219+ 'serve:Start dev server'
3220+ 'watch:Watch and recompile'
3221+ 'check:Check for errors'
3222+ 'convert:Convert HTML to .hl'
3223+ 'fmt:Format a .hl file'
3224+ 'sitemap:Generate sitemap.xml'
3225+ 'lint:Lint checks'
3226+ 'stats:Show file statistics'
3227+ 'preview:Compile and open in browser'
3228+ 'diff:Compare two .hl files'
3229+ 'export:Bundle into archive'
3230+ 'repl:Interactive REPL'
3231+ 'feed:Generate RSS feed'
3232+ 'components:List @fn definitions'
3233+ 'deps:Show dependency graph'
3234+ 'dead-code:Find unused definitions'
3235+ 'deploy:Build and deploy'
3236+ 'playground:Generate playground'
3237+ 'clean:Remove generated files'
3238+ 'upgrade:Auto-upgrade syntax'
3239+ 'create-component:Scaffold component'
3240+ 'outline:Show document structure'
3241+ 'doctor:Check toolchain health'
3242+ 'migrate:Upgrade deprecated syntax'
3243+ 'bundle:Inline all assets'
3244+ 'size:Report output sizes'
3245+ 'benchmark:Measure compile time'
3246+ 'test:Run assertions'
3247+ 'lsp:Start LSP server'
3248+ 'completions:Generate shell completions'
3249+ )
3250+
3251+ local -a flags=(
3252+ '-w[Watch for changes]'
3253+ '--watch[Watch for changes]'
3254+ '-s[Start dev server]'
3255+ '--serve[Start dev server]'
3256+ '-d[Development mode]'
3257+ '--dev[Development mode]'
3258+ '-c[Check only]'
3259+ '--check[Check only]'
3260+ '--compat[Vendor prefixes]'
3261+ '--strict[Warnings as errors]'
3262+ '--partial[Output HTML fragment]'
3263+ '--open[Open browser]'
3264+ '--minify[Minify output]'
3265+ '-o[Output path]:path:_files'
3266+ '--output[Output path]:path:_files'
3267+ '-p[Port]:port:'
3268+ '--port[Port]:port:'
3269+ '--format[Output format]:format:(json)'
3270+ '-h[Show help]'
3271+ '--help[Show help]'
3272+ '-V[Show version]'
3273+ '--version[Show version]'
3274+ )
3275+
3276+ if (( CURRENT == 2 )); then
3277+ _alternative 'commands:command:compadd -a commands' 'files:file:_files -g "*.hl"' 'dirs:directory:_directories'
3278+ else
3279+ _alternative 'flags:flag:compadd -a flags' 'files:file:_files -g "*.hl"' 'dirs:directory:_directories'
3280+ fi
3281+}}
3282+
3283+_htmlang "$@""#);
3284+}
3285+
3286+fn print_fish_completions() {
3287+ let commands = [
3288+ ("init", "Create a new project"),
3289+ ("new", "Create a new .hl page"),
3290+ ("build", "Compile all .hl files"),
3291+ ("serve", "Start dev server with hot reload"),
3292+ ("watch", "Watch for changes and recompile"),
3293+ ("check", "Check for errors"),
3294+ ("convert", "Convert HTML to .hl"),
3295+ ("fmt", "Format a .hl file"),
3296+ ("sitemap", "Generate sitemap.xml"),
3297+ ("lint", "Strict lint checks"),
3298+ ("stats", "Show file statistics"),
3299+ ("preview", "Compile and open in browser"),
3300+ ("diff", "Compare two .hl files"),
3301+ ("export", "Bundle into archive"),
3302+ ("repl", "Interactive REPL"),
3303+ ("feed", "Generate RSS feed"),
3304+ ("components", "List @fn definitions"),
3305+ ("deps", "Show dependency graph"),
3306+ ("dead-code", "Find unused definitions"),
3307+ ("deploy", "Build and deploy"),
3308+ ("playground", "Generate playground"),
3309+ ("clean", "Remove generated files"),
3310+ ("upgrade", "Auto-upgrade syntax"),
3311+ ("create-component", "Scaffold component"),
3312+ ("outline", "Show document structure"),
3313+ ("doctor", "Check toolchain health"),
3314+ ("migrate", "Upgrade deprecated syntax"),
3315+ ("bundle", "Inline all assets as data URIs"),
3316+ ("size", "Report output sizes"),
3317+ ("benchmark", "Measure compile time"),
3318+ ("test", "Run assertions"),
3319+ ("lsp", "Start LSP server"),
3320+ ("completions", "Generate shell completions"),
3321+ ];
3322+
3323+ for (cmd, desc) in &commands {
3324+ println!("complete -c htmlang -n '__fish_use_subcommand' -a '{}' -d '{}'", cmd, desc);
3325+ }
3326+
3327+ let flags = [
3328+ ("-w", "--watch", "Watch for changes"),
3329+ ("-s", "--serve", "Start dev server"),
3330+ ("-d", "--dev", "Development mode"),
3331+ ("-c", "--check", "Check only"),
3332+ ("", "--compat", "Add vendor prefixes"),
3333+ ("", "--strict", "Treat warnings as errors"),
3334+ ("", "--partial", "Output HTML fragment without document wrapper"),
3335+ ("", "--open", "Open browser"),
3336+ ("", "--minify", "Minify output"),
3337+ ("-o", "--output", "Output path"),
3338+ ("-p", "--port", "Dev server port"),
3339+ ("-h", "--help", "Show help"),
3340+ ("-V", "--version", "Show version"),
3341+ ];
3342+
3343+ for (short, long, desc) in &flags {
3344+ if short.is_empty() {
3345+ println!("complete -c htmlang -l '{}' -d '{}'", long.trim_start_matches('-'), desc);
3346+ } else {
3347+ println!("complete -c htmlang -s '{}' -l '{}' -d '{}'", short.trim_start_matches('-'), long.trim_start_matches('-'), desc);
3348+ }
3349+ }
3350+
3351+ println!("complete -c htmlang -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'");
3352+}
3353+
30993354fn collect_hl_files(dir: &Path) -> Vec<PathBuf> {
31003355 let mut files = Vec::new();
31013356 if let Ok(entries) = fs::read_dir(dir) {