Skip to content

feat: apply the nearest configuration file to each file (nested configs) - #881

Open
psteinroe wants to merge 3 commits into
mainfrom
feat/nested-configs
Open

psteinroe wants to merge 3 commits into
mainfrom
feat/nested-configs

Conversation

@psteinroe

@psteinroe psteinroe commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Addresses #719 (see also #326 and #293).

Problem

The language server loaded one configuration file per LSP workspace folder, searching upward from the folder root. A postgres-language-server.jsonc in a subdirectory was ignored. So a repo with src/db1 and src/db2 pointing at different databases could only be configured through a multi-root workspace. The CLI likewise used only the working directory's configuration.

What changed

Each file now uses the nearest postgres-language-server.jsonc at or above it, within the workspace root, like Biome's nested configs.

my-repo/
├── postgres-language-server.jsonc      # applies to everything else
└── src/
    ├── db1/postgres-language-server.jsonc  # applies to src/db1/**
    └── db2/postgres-language-server.jsonc  # applies to src/db2/**
  • Discovery (pgls_workspace::configuration::register_nested_projects): finds configuration files below a root through the new FileSystem::find_files. On the OS file system it uses ignore::WalkBuilder, so it respects .gitignore/.ignore, skips hidden directories, and skips node_modules/target. Each configuration file is registered as its own project via register_project_folder + update_settings, shallowest first. A directory that the project above it ignores (files.ignore/files.include) is skipped. The root project is the current project again afterwards.
  • Project selection (WorkspaceSettings::get_project_key_for_path): every per-file operation (diagnostics, formatting, completions, hover, code actions, execute statement, is_path_ignored, migrations) now uses the settings of the project with the deepest folder containing the path. Before, it used a mutable "current project" that open_file switched as a side effect. That was racy across documents and picked the first prefix match, not the longest. A relative path (CLI arguments) resolves against the current project's folder, which is the working directory. Paths outside every project keep using the current project, so single-project behaviour is unchanged.
  • register_project_folder now reuses an existing project with the same path instead of only comparing against the current one. Before, re-registering after switching projects added duplicates.
  • LSP: after loading a workspace folder's (or root URI's) configuration, the session registers nested projects with the same layers as the root: environment config, sticky client overrides, and section reset. On every reload it first unregisters the nested projects of the previous load, so deleted or newly ignored configs disappear. The didChangeWatchedFiles registration now watches <root>/**/postgres-language-server.jsonc. Any config-file event reloads the settings and now refreshes the diagnostics of open documents. That refresh was commented out before, so diagnostics stayed stale until the next edit.
  • CLI: check and format register nested projects below the working directory. Env vars and CLI arguments are layered over each nested config, the same way as over the root config. check_database_connection checks every project's database.
  • Database / performance: connection pools and schema snapshots were already keyed by connection parameters (ConnectionKey), so per-project databases needed no new plumbing. Pools are created lazily (connect_lazy_with), so no connection is opened until a file of that project needs one. Projects pointing at the same database share the pool and snapshot. snapshot_before now takes the file's search_path explicitly instead of reading the current project's.

Inheritance: explicit only

A nested configuration inherits nothing implicitly; unset settings are defaults. To share settings, use the existing extends:

// src/db1/postgres-language-server.jsonc
{
  "extends": ["../../postgres-language-server.jsonc"],
  "db": { "database": "db1" }
}

I chose this over Biome's root: false semantics to keep it simple, predictable, and free of new configuration surface. extends already merges with the nested file's own settings taking precedence. Relative paths such as migrations.migrationsDir resolve against the nested file's folder.

Interaction with the user configuration (#878)

#878 falls back to a user-level config when no project config is found at or above the project directory. With nested configs:

  • The fallback stays a root-only fallback. If there is no config at or above the workspace root (or CLI working directory), the user config becomes the root project's config. It then applies to every file that no nested config covers.
  • Nested projects never merge the user config implicitly. A nested project always has its own file, so load_configuration never reaches the fallback for it. To use the user config there, extend it explicitly, e.g. "extends": ["~/.config/postgres-language-server/postgres-language-server.jsonc"] (~ expansion is from feat(configuration): expand ~ in extends and fall back to a user configuration #878).
  • The user config directories are excluded from nested discovery. Otherwise opening ~ as a workspace on macOS would register ~/Library/Application Support/postgres-language-server/ as a nested project, because that folder isn't hidden.

Covered by nested_projects_do_not_merge_the_user_configuration: a workspace at the home directory with no project config and a user config that sets banDropColumn: error. A file without a project config gets error. A nested {} project gets the default warning. A nested project that extends: ["~/…"] gets error. The user config dir is not registered as a project.

Deliberately out of scope

  • --config-path (CLI) and the LSP's explicit configuration path: an explicitly chosen file still applies to everything, and nested files are not looked up.
  • dblint (not file-based) uses the working directory's configuration.
  • files.ignore/include globs are matched as before. The matcher does not anchor globs to the config's folder, which is existing behaviour and not changed here, so the docs don't promise it.
  • Requests not tied to a file (invalidate_schema_cache without all, pull_db_diagnostics) still use the current/root project.

Risks

  • Behaviour change for repos that already contain configuration files in subdirectories (for example an example or fixture folder): files there now use that config instead of the root one. Hidden folders, node_modules, target, gitignored folders, and folders ignored by the root config are excluded, which should cover most accidental cases.
  • Discovery walks the workspace on every configuration load. It is gitignore-aware and only reads matching file names, but a very large non-ignored tree adds startup cost.
  • The project-selection rewrite touches every per-file workspace call; covered by the existing workspace/LSP/CLI suites plus the new tests.

Tests

  • pgls_workspace settings unit tests: longest-prefix selection (incl. db1 vs db10), relative paths, the unnamed project as the least specific, fallback to the current project, and removing the current project.
  • pgls_workspace server tests: nested_projects_do_not_merge_the_user_configuration (see above). nested_projects_use_their_own_migrations_settings puts the same dbmate migration in a nested project (sectionMarkers: dbmate, lintDown: false, migrationsDir, after) and in a root without migrations settings. Only the nested one skips the down section, and after only ignores files in the nested migrations/ folder. nested_projects_use_the_nearest_configuration_file covers per-directory linter settings, a deeper override, a config in a root-ignored folder that is skipped, and one under node_modules that is never loaded. registering_a_project_folder_again_reuses_its_project.
  • LSP (crates/pgls_lsp/tests/server.rs): nested_configuration_files_apply_to_their_directories uses a temp workspace with a root config and two nested configs. It checks different banDropColumn severities per directory, picks up a config created at runtime, and re-publishes diagnostics after a nested config is deleted.
  • CLI (assert_check.rs): check_uses_the_nearest_configuration_file covers directory traversal and relative path arguments, including a nested config that extends the root.

The tests use linter settings rather than two databases. Database selection goes through the same per-file Settings lookup, and pools/snapshots were already keyed by connection.

@psteinroe
psteinroe force-pushed the feat/nested-configs branch from 818bd74 to 057e50f Compare October 19, 2026 12:35
@psteinroe
psteinroe force-pushed the feat/nested-configs branch from 057e50f to 1485b46 Compare October 19, 2026 13:05

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant