From cb44d0a47c3a71e23fec94fa7413507360b0cbd4 Mon Sep 17 00:00:00 2001 From: JohnnyMorganz Date: Sun, 21 Sep 2025 13:51:56 +0200 Subject: [PATCH 1/2] Check against styluaignore in LSP --- src/cli/lsp.rs | 90 +++++++++++++++++++++++++++++++++------- src/cli/main.rs | 73 +++++--------------------------- src/cli/stylua_ignore.rs | 69 ++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 77 deletions(-) create mode 100644 src/cli/stylua_ignore.rs diff --git a/src/cli/lsp.rs b/src/cli/lsp.rs index 70dee7c1..0d516a7a 100644 --- a/src/cli/lsp.rs +++ b/src/cli/lsp.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; use similar::{DiffOp, TextDiff}; use stylua_lib::{format_code, IndentType, OutputVerification}; -use crate::{config::ConfigResolver, opt}; +use crate::{config::ConfigResolver, opt, stylua_ignore}; fn diffop_to_textedit( op: DiffOp, @@ -64,6 +64,7 @@ struct LanguageServer<'a> { documents: TextDocuments, workspace_folders: Vec, root_uri: Option, + search_parent_directories: bool, respect_editor_formatting_options: bool, config_resolver: &'a mut ConfigResolver<'a>, } @@ -72,12 +73,14 @@ enum FormattingError { StyLuaError, NotLuaDocument, DocumentNotFound, + FileIsIgnored, } impl LanguageServer<'_> { fn new<'a>( workspace_folders: Vec, root_uri: Option, + search_parent_directories: bool, respect_editor_formatting_options: bool, config_resolver: &'a mut ConfigResolver<'a>, ) -> LanguageServer<'a> { @@ -85,6 +88,7 @@ impl LanguageServer<'_> { documents: TextDocuments::new(), workspace_folders, root_uri, + search_parent_directories, respect_editor_formatting_options, config_resolver, } @@ -140,14 +144,24 @@ impl LanguageServer<'_> { return Err(FormattingError::NotLuaDocument); } + let search_root = Some(self.find_config_root(uri)); + let path = uri.path().as_str().as_ref(); + + if stylua_ignore::path_is_stylua_ignored( + path, + self.search_parent_directories, + search_root.clone(), + ) + .unwrap_or(false) + { + return Err(FormattingError::FileIsIgnored); + } + let contents = document.get_content(None); let mut config = self .config_resolver - .load_configuration_with_search_root( - uri.path().as_str().as_ref(), - Some(self.find_config_root(uri)), - ) + .load_configuration_with_search_root(path, search_root) .unwrap_or_default(); if let Some(formatting_options) = formatting_options { @@ -193,7 +207,8 @@ impl LanguageServer<'_> { ) { Ok(edits) => Response::new_ok(request.id, edits), Err(FormattingError::StyLuaError) - | Err(FormattingError::NotLuaDocument) => { + | Err(FormattingError::NotLuaDocument) + | Err(FormattingError::FileIsIgnored) => { Response::new_ok(request.id, serde_json::Value::Null) } Err(FormattingError::DocumentNotFound) => Response::new_err( @@ -224,7 +239,8 @@ impl LanguageServer<'_> { ) { Ok(edits) => Response::new_ok(request.id, edits), Err(FormattingError::StyLuaError) - | Err(FormattingError::NotLuaDocument) => { + | Err(FormattingError::NotLuaDocument) + | Err(FormattingError::FileIsIgnored) => { Response::new_ok(request.id, serde_json::Value::Null) } Err(FormattingError::DocumentNotFound) => Response::new_err( @@ -266,6 +282,7 @@ struct InitializationOptions { fn main_loop<'a>( connection: Connection, + search_parent_directories: bool, config_resolver: &'a mut ConfigResolver<'a>, ) -> anyhow::Result<()> { let initialize_result = InitializeResult { @@ -298,6 +315,7 @@ fn main_loop<'a>( initialize_params.workspace_folders.unwrap_or_default(), #[allow(deprecated)] initialize_params.root_uri, + search_parent_directories, respect_editor_formatting_options, config_resolver, ); @@ -328,7 +346,11 @@ pub fn run(opt: opt::Opt) -> anyhow::Result<()> { let (connection, io_threads) = Connection::stdio(); - main_loop(connection, &mut config_resolver)?; + main_loop( + connection, + opt.search_parent_directories, + &mut config_resolver, + )?; io_threads.join()?; @@ -396,7 +418,7 @@ mod tests { client.sender.send($messages).unwrap(); )* - main_loop(server, &mut config_resolver).unwrap(); + main_loop(server, false, &mut config_resolver).unwrap(); $( $tests(&client.receiver); @@ -540,7 +562,7 @@ mod tests { client.sender.send(shutdown(2)).unwrap(); client.sender.send(exit()).unwrap(); - main_loop(server, &mut config_resolver).unwrap(); + main_loop(server, false, &mut config_resolver).unwrap(); expect_server_initialized(&client.receiver, 1); expect_server_shutdown(&client.receiver, 2); @@ -602,7 +624,7 @@ mod tests { client.sender.send(shutdown(3)).unwrap(); client.sender.send(exit()).unwrap(); - main_loop(server, &mut config_resolver).unwrap(); + main_loop(server, false, &mut config_resolver).unwrap(); expect_server_initialized(&client.receiver, 1); @@ -667,7 +689,7 @@ mod tests { client.sender.send(shutdown(3)).unwrap(); client.sender.send(exit()).unwrap(); - main_loop(server, &mut config_resolver).unwrap(); + main_loop(server, false, &mut config_resolver).unwrap(); expect_server_initialized(&client.receiver, 1); @@ -751,7 +773,7 @@ mod tests { client.sender.send(shutdown(3)).unwrap(); client.sender.send(exit()).unwrap(); - main_loop(server, &mut config_resolver).unwrap(); + main_loop(server, false, &mut config_resolver).unwrap(); expect_server_initialized(&client.receiver, 1); @@ -783,7 +805,7 @@ mod tests { client.sender.send(shutdown(3)).unwrap(); client.sender.send(exit()).unwrap(); - main_loop(server, &mut config_resolver).unwrap(); + main_loop(server, false, &mut config_resolver).unwrap(); expect_server_initialized(&client.receiver, 1); @@ -1047,4 +1069,44 @@ mod tests { ] ); } + + #[test] + fn test_lsp_stylua_ignore() { + let contents = "local x = 1"; + let cwd = construct_tree!({ + ".styluaignore": "ignored/", + "foo.lua": contents, + "ignored/bar.lua": contents, + }); + + let foo_uri = Uri::from_str(cwd.child("foo.lua").to_str().unwrap()).unwrap(); + let bar_uri = Uri::from_str(cwd.child("ignored/bar.lua").to_str().unwrap()).unwrap(); + + lsp_test!( + [], + [ + initialize(1, Some(cwd.path())), + initialized(), + open_text_document(foo_uri.clone(), contents.to_string()), + open_text_document(bar_uri.clone(), contents.to_string()), + format_document(2, foo_uri.clone(), FormattingOptions::default()), + format_document(3, bar_uri.clone(), FormattingOptions::default()), + shutdown(4), + exit() + ], + [ + |receiver| expect_server_initialized(receiver, 1), + |receiver| { + let edits: Vec = expect_response(receiver, 2); + let formatted = apply_text_edits_to(contents, edits); + assert_eq!(formatted, "local x = 1\n"); + }, + |receiver| { + let edits: serde_json::Value = expect_response(receiver, 3); + assert_eq!(edits, serde_json::Value::Null); + }, + |receiver| expect_server_shutdown(receiver, 4) + ] + ); + } } diff --git a/src/cli/main.rs b/src/cli/main.rs index 8c2d01e4..a026b13b 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -1,7 +1,7 @@ use anyhow::{bail, Context, Result}; use clap::StructOpt; use console::style; -use ignore::{gitignore::Gitignore, overrides::OverrideBuilder, WalkBuilder}; +use ignore::{overrides::OverrideBuilder, WalkBuilder}; use log::{LevelFilter, *}; use serde_json::json; use std::collections::HashSet; @@ -16,13 +16,14 @@ use threadpool::ThreadPool; use stylua_lib::{format_code, Config, OutputVerification, Range}; -use crate::config::find_ignore_file_path; - mod config; #[cfg(feature = "lsp")] mod lsp; mod opt; mod output_diff; +mod stylua_ignore; + +use stylua_ignore::{is_explicitly_provided, path_is_stylua_ignored, should_respect_ignores}; static EXIT_CODE: AtomicI32 = AtomicI32::new(0); static UNFORMATTED_FILE_COUNT: AtomicU32 = AtomicU32::new(0); @@ -206,64 +207,6 @@ fn format_string( } } -fn get_ignore( - directory: &Path, - search_parent_directories: bool, -) -> Result { - let file_path = find_ignore_file_path(directory.to_path_buf(), search_parent_directories) - .or_else(|| { - std::env::current_dir() - .ok() - .and_then(|cwd| find_ignore_file_path(cwd, false)) - }); - - if let Some(file_path) = file_path { - let (ignore, err) = Gitignore::new(file_path); - if let Some(err) = err { - Err(err) - } else { - Ok(ignore) - } - } else { - Ok(Gitignore::empty()) - } -} - -/// Whether the provided path was explicitly provided to the tool -fn is_explicitly_provided(opt: &opt::Opt, path: &Path) -> bool { - opt.files.iter().any(|p| path == *p) -} - -/// By default, files explicitly passed to the command line will be formatted regardless of whether -/// they are present in .styluaignore / not glob matched. If `--respect-ignores` is provided, -/// then we enforce .styluaignore / glob matching on explicitly passed paths. -fn should_respect_ignores(opt: &opt::Opt, path: &Path) -> bool { - !is_explicitly_provided(opt, path) || opt.respect_ignores -} - -fn path_is_stylua_ignored(path: &Path, search_parent_directories: bool) -> Result { - let ignore = get_ignore( - path.parent().expect("cannot get parent directory"), - search_parent_directories, - ) - .context("failed to parse ignore file")?; - - // matched_path_or_any_parents panics when path is not in cwd - // can happen when `--respect-ignores --stdin-filepath {path}` - if !path - .canonicalize() - .unwrap_or_default() - .starts_with(ignore.path().canonicalize().unwrap_or_default()) - { - return Ok(false); - } - - Ok(matches!( - ignore.matched_path_or_any_parents(path, false), - ignore::Match::Ignore(_) - )) -} - fn format(opt: opt::Opt) -> Result { debug!("resolved options: {:#?}", opt); @@ -436,7 +379,11 @@ fn format(opt: opt::Opt) -> Result { let should_skip_format = match &opt.stdin_filepath { Some(path) => { opt.respect_ignores - && path_is_stylua_ignored(path, opt.search_parent_directories)? + && path_is_stylua_ignored( + path, + opt.search_parent_directories, + None, + )? } None => false, }; @@ -501,7 +448,7 @@ fn format(opt: opt::Opt) -> Result { // we should check .styluaignore if is_explicitly_provided(opt.as_ref(), &path) && should_respect_ignores(opt.as_ref(), &path) - && path_is_stylua_ignored(&path, opt.search_parent_directories)? + && path_is_stylua_ignored(&path, opt.search_parent_directories, None)? { continue; } diff --git a/src/cli/stylua_ignore.rs b/src/cli/stylua_ignore.rs new file mode 100644 index 00000000..59a3d735 --- /dev/null +++ b/src/cli/stylua_ignore.rs @@ -0,0 +1,69 @@ +use crate::config::find_ignore_file_path; +use crate::opt::Opt; +use anyhow::{Context, Result}; +use ignore::gitignore::Gitignore; +use std::path::{Path, PathBuf}; + +fn get_ignore( + directory: &Path, + search_parent_directories: bool, + search_root: Option, +) -> Result { + let file_path = find_ignore_file_path(directory.to_path_buf(), search_parent_directories) + .or_else(|| { + search_root + .or_else(|| std::env::current_dir().ok()) + .and_then(|cwd| find_ignore_file_path(cwd, false)) + }); + + if let Some(file_path) = file_path { + let (ignore, err) = Gitignore::new(file_path); + if let Some(err) = err { + Err(err) + } else { + Ok(ignore) + } + } else { + Ok(Gitignore::empty()) + } +} + +/// Whether the provided path was explicitly provided to the tool +pub fn is_explicitly_provided(opt: &Opt, path: &Path) -> bool { + opt.files.iter().any(|p| path == *p) +} + +/// By default, files explicitly passed to the command line will be formatted regardless of whether +/// they are present in .styluaignore / not glob matched. If `--respect-ignores` is provided, +/// then we enforce .styluaignore / glob matching on explicitly passed paths. +pub fn should_respect_ignores(opt: &Opt, path: &Path) -> bool { + !is_explicitly_provided(opt, path) || opt.respect_ignores +} + +pub fn path_is_stylua_ignored( + path: &Path, + search_parent_directories: bool, + search_root: Option, +) -> Result { + let ignore = get_ignore( + path.parent().expect("cannot get parent directory"), + search_parent_directories, + search_root, + ) + .context("failed to parse ignore file")?; + + // matched_path_or_any_parents panics when path is not in cwd + // can happen when `--respect-ignores --stdin-filepath {path}` + if !path + .canonicalize() + .unwrap_or_default() + .starts_with(ignore.path().canonicalize().unwrap_or_default()) + { + return Ok(false); + } + + Ok(matches!( + ignore.matched_path_or_any_parents(path, false), + ignore::Match::Ignore(_) + )) +} From 6893d78f8e050b7ad144a55f5b60a66826f981f8 Mon Sep 17 00:00:00 2001 From: JohnnyMorganz Date: Sun, 21 Sep 2025 13:52:00 +0200 Subject: [PATCH 2/2] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d9eb45f..9e8597dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed comments lost from expression after parentheses are removed when we are attempting to "hang" the expression. ([#1033](https://github.com/JohnnyMorganz/StyLua/issues/1033)) - Fixed `document_range_formatting_provider` capability missing from `ServerCapabilities` in language server mode - Fixed current working directory incorrectly used as config search root in language server mode -- now, the root of the opened workspace is used instead ([#1032](https://github.com/JohnnyMorganz/StyLua/issues/1032)) +- Language server mode now correctly respects `.styluaignore` files ([#1035](https://github.com/JohnnyMorganz/StyLua/issues/1035)) ## [2.2.0] - 2025-09-14