pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
561 lines (556 loc) • 16.8 kB
JavaScript
import { createRequire as __pilensCreateRequire } from "node:module"; const require = __pilensCreateRequire(import.meta.url);
import {
getSourceFiles
} from "./chunk-GVXA6CCE.js";
import {
readDirEntriesSafe,
shouldRecurseIntoDir
} from "./chunk-VHQLPIRJ.js";
import {
getProjectIgnoreMatcher
} from "./chunk-VN2AXLEX.js";
import {
CODE_KINDS,
DOTNET_CSHARP_ROOT_MARKERS,
DOTNET_FSHARP_ROOT_MARKERS,
KIND_EXTENSIONS,
TERRAGRUNT_FILENAMES,
detectFileKind
} from "./chunk-5ZEJHV35.js";
import {
findNearestDirWithAnyBasename,
registerWorkspaceTopologyReset
} from "./chunk-VNMT7C64.js";
import {
BoundedLruCache
} from "./chunk-ABBOA7UD.js";
import {
direntsHaveMarkerGlobMatch,
normalizeMapKey
} from "./chunk-Q5U6FMDI.js";
// dist/clients/language-profile.js
import * as fs from "node:fs";
import * as path from "node:path";
// dist/clients/language-policy.js
var LANGUAGE_POLICY = {
jsts: {
lspCapable: true,
startup: {
defaults: ["typescript-language-server", "biome"],
heavyScansRequireConfig: true
}
},
python: {
lspCapable: true,
startup: {
defaults: ["pyright", "ruff"]
}
},
go: { lspCapable: true },
rust: { lspCapable: true },
cxx: { lspCapable: true },
cmake: { lspCapable: true },
fish: { lspCapable: true },
shell: { lspCapable: true, startup: { defaults: ["shellcheck"] } },
json: { lspCapable: true },
markdown: { lspCapable: true },
css: { lspCapable: true },
yaml: {
lspCapable: true,
startup: {
defaults: ["yamllint"],
heavyScansRequireConfig: true
}
},
sql: {
lspCapable: false,
startup: {
defaults: ["sqlfluff"],
heavyScansRequireConfig: true
}
},
ruby: {
lspCapable: true,
startup: {
defaults: ["rubocop"],
heavyScansRequireConfig: true
}
},
html: { lspCapable: true },
docker: { lspCapable: true },
php: { lspCapable: true },
powershell: { lspCapable: true },
prisma: { lspCapable: true },
csharp: { lspCapable: true },
fsharp: { lspCapable: true },
java: { lspCapable: true },
kotlin: { lspCapable: true, startup: { defaults: ["ktlint"] } },
swift: { lspCapable: true },
dart: { lspCapable: true },
lua: { lspCapable: true },
zig: { lspCapable: true },
haskell: { lspCapable: true },
"helm-template": { lspCapable: false },
elixir: { lspCapable: true },
gleam: { lspCapable: true },
ocaml: { lspCapable: true },
clojure: { lspCapable: true },
cue: { lspCapable: true },
terraform: { lspCapable: true },
terragrunt: { lspCapable: false },
nix: { lspCapable: true },
toml: { lspCapable: true, startup: { defaults: ["taplo"] } }
};
var PRIMARY_DISPATCH_GROUPS = {
jsts: {
mode: "fallback",
runnerIds: ["lsp"],
filterKinds: ["jsts"]
},
python: {
mode: "all",
runnerIds: ["lsp", "pyright"],
filterKinds: ["python"]
},
go: { mode: "all", runnerIds: ["lsp", "go-vet"], filterKinds: ["go"] },
rust: {
mode: "all",
runnerIds: ["lsp", "rust-clippy"],
filterKinds: ["rust"]
},
ruby: {
mode: "all",
runnerIds: ["lsp", "rubocop"],
filterKinds: ["ruby"]
},
cxx: {
mode: "all",
runnerIds: ["lsp", "cpp-check"],
filterKinds: ["cxx"]
},
cmake: { mode: "fallback", runnerIds: ["lsp"], filterKinds: ["cmake"] },
fish: {
mode: "all",
runnerIds: ["lsp", "fish-indent"],
filterKinds: ["fish"]
},
shell: {
mode: "all",
runnerIds: ["lsp", "shellcheck"],
filterKinds: ["shell"]
},
json: { mode: "fallback", runnerIds: ["lsp"], filterKinds: ["json"] },
markdown: {
// marksman (primary LSP) adds cross-file checks the cold spellcheck/vale
// runners can't see; all three run so prose + structural coverage coexist.
mode: "all",
runnerIds: ["lsp", "spellcheck", "vale"],
filterKinds: ["markdown"]
},
css: {
mode: "all",
runnerIds: ["lsp", "stylelint"],
filterKinds: ["css"]
},
yaml: {
mode: "all",
runnerIds: ["lsp", "yamllint", "trivy-config", "helm-lint", "helm-render"],
filterKinds: ["yaml"]
},
sql: {
mode: "fallback",
runnerIds: ["sqlfluff"],
filterKinds: ["sql"]
},
html: {
mode: "all",
runnerIds: ["lsp", "htmlhint"],
filterKinds: ["html"]
},
docker: {
mode: "all",
runnerIds: ["lsp", "hadolint", "trivy-config"],
filterKinds: ["docker"]
},
php: {
mode: "fallback",
runnerIds: ["lsp", "php-lint", "phpstan"],
filterKinds: ["php"]
},
powershell: {
mode: "all",
runnerIds: ["lsp", "psscriptanalyzer"],
filterKinds: ["powershell"]
},
prisma: {
mode: "all",
runnerIds: ["lsp", "prisma-validate"],
filterKinds: ["prisma"]
},
csharp: {
mode: "fallback",
runnerIds: ["lsp", "dotnet-build"],
filterKinds: ["csharp"]
},
fsharp: { mode: "fallback", runnerIds: ["lsp"], filterKinds: ["fsharp"] },
java: {
mode: "fallback",
runnerIds: ["lsp", "javac"],
filterKinds: ["java"]
},
kotlin: {
mode: "all",
runnerIds: ["lsp", "ktlint"],
filterKinds: ["kotlin"]
},
swift: {
mode: "all",
runnerIds: ["lsp", "swiftlint"],
filterKinds: ["swift"]
},
dart: {
mode: "all",
runnerIds: ["lsp", "dart-analyze"],
filterKinds: ["dart"]
},
lua: { mode: "fallback", runnerIds: ["lsp"], filterKinds: ["lua"] },
zig: { mode: "all", runnerIds: ["lsp", "zig-check"], filterKinds: ["zig"] },
haskell: { mode: "fallback", runnerIds: ["lsp"], filterKinds: ["haskell"] },
"helm-template": {
mode: "all",
// helm-render is opt-in (`helm.renderValidation.enabled`) and skips itself
// when the switch is absent, so it costs nothing when unconfigured (#1283).
runnerIds: ["helm-lint", "helm-render"],
filterKinds: ["helm-template"]
},
elixir: {
mode: "all",
runnerIds: ["lsp", "elixir-check", "credo"],
filterKinds: ["elixir"]
},
gleam: {
mode: "all",
runnerIds: ["lsp", "gleam-check"],
filterKinds: ["gleam"]
},
ocaml: { mode: "fallback", runnerIds: ["lsp"], filterKinds: ["ocaml"] },
clojure: { mode: "fallback", runnerIds: ["lsp"], filterKinds: ["clojure"] },
// #1522: cuelsp reports syntax/parse errors only; cue-vet covers the
// evaluation-error class the language server deliberately doesn't publish.
cue: { mode: "all", runnerIds: ["lsp", "cue-vet"], filterKinds: ["cue"] },
terraform: {
mode: "all",
runnerIds: ["lsp", "tflint", "trivy-config"],
filterKinds: ["terraform"]
},
terragrunt: {
mode: "all",
runnerIds: ["terragrunt"],
filterKinds: ["terragrunt"]
},
nix: { mode: "fallback", runnerIds: ["lsp"], filterKinds: ["nix"] },
toml: {
mode: "all",
runnerIds: ["lsp", "taplo"],
filterKinds: ["toml"]
}
};
function getLspCapableKinds() {
return Object.keys(LANGUAGE_POLICY).filter((kind) => LANGUAGE_POLICY[kind].lspCapable);
}
function getPrimaryDispatchGroup(kind, lspEnabled) {
const base = PRIMARY_DISPATCH_GROUPS[kind];
if (!base)
return void 0;
const ids = lspEnabled ? [...base.runnerIds] : base.runnerIds.filter((id) => id !== "lsp");
if (ids.length === 0)
return void 0;
return {
mode: base.mode,
runnerIds: ids,
filterKinds: base.filterKinds,
semantic: base.semantic
};
}
function canRunStartupHeavyScans(profile, kind) {
if (!profile.present[kind])
return false;
const needsConfig = LANGUAGE_POLICY[kind].startup?.heavyScansRequireConfig;
if (!needsConfig)
return true;
return !!profile.configured[kind];
}
// dist/clients/language-profile.js
var SUPPORTED_FILE_KINDS = Object.keys(KIND_EXTENSIONS);
var PROJECT_MARKERS_BY_KIND = {
jsts: ["package.json", "tsconfig.json", "jsconfig.json"],
python: ["pyproject.toml", "requirements.txt", "setup.py", "setup.cfg"],
go: ["go.mod"],
rust: ["Cargo.toml"],
cxx: [
"compile_commands.json",
"compile_flags.txt",
".clangd",
"CMakeLists.txt",
"Makefile",
"makefile",
"meson.build",
"build.ninja"
],
ruby: ["Gemfile", "Rakefile"],
yaml: [".yamllint", "yamllint.yaml", "yamllint.yml", "pyproject.toml"],
sql: [".sqlfluff", "pyproject.toml"],
php: ["composer.json", "composer.lock"],
prisma: ["schema.prisma", "prisma/schema.prisma"],
// #2870: `build.gradle.kts` is the Kotlin-DSL spelling of `build.gradle`
// and marks a Java module just as well — its absence here (and in
// ROOT_MARKERS_BY_KIND below) is what left a `.java` file in a
// Kotlin-DSL Gradle module resolving to the workspace root, where a
// polyglot repo's go.mod then claimed it. `kotlin` already lists both
// spellings; java listed only the Groovy one.
java: ["pom.xml", "build.gradle", "build.gradle.kts", ".classpath"],
kotlin: ["build.gradle.kts", "build.gradle", "pom.xml"],
swift: ["Package.swift"],
dart: ["pubspec.yaml"],
elixir: ["mix.exs"],
cue: ["cue.mod"],
gleam: ["gleam.toml"],
terraform: [".terraform.lock.hcl"],
terragrunt: TERRAGRUNT_FILENAMES,
nix: ["flake.nix"],
toml: ["pyproject.toml", "Cargo.toml", "taplo.toml"],
csharp: DOTNET_CSHARP_ROOT_MARKERS,
fsharp: DOTNET_FSHARP_ROOT_MARKERS
};
var ROOT_MARKERS_BY_KIND = {
jsts: [
"biome.json",
"biome.jsonc",
"package.json",
"tsconfig.json",
"jsconfig.json",
"pnpm-workspace.yaml"
],
python: [
"pyproject.toml",
"requirements.txt",
"setup.py",
"setup.cfg",
"Pipfile"
],
go: ["go.work", "go.mod", "go.sum"],
rust: ["Cargo.toml"],
cxx: [
"compile_commands.json",
"compile_flags.txt",
".clangd",
"CMakeLists.txt",
"Makefile",
"makefile",
"meson.build",
"build.ninja",
".git"
],
ruby: ["Gemfile", "Rakefile"],
yaml: [".yamllint", ".yamllint.yml", ".yamllint.yaml"],
sql: [".sqlfluff", "pyproject.toml", "setup.cfg", "tox.ini"],
php: ["composer.json", "composer.lock"],
prisma: ["prisma/schema.prisma", "schema.prisma"],
// #2870: see PROJECT_MARKERS_BY_KIND.java above — this is the table
// `resolveLanguageRootForFile` walks, so the missing Kotlin-DSL spelling
// is what anchored a nested Gradle module's `.java` file at the
// workspace root.
java: ["pom.xml", "build.gradle", "build.gradle.kts", ".classpath"],
kotlin: ["build.gradle.kts", "build.gradle", "pom.xml"],
swift: ["Package.swift"],
dart: ["pubspec.yaml"],
elixir: ["mix.exs"],
cue: ["cue.mod", ".git"],
gleam: ["gleam.toml"],
terraform: [".terraform.lock.hcl"],
terragrunt: TERRAGRUNT_FILENAMES,
nix: ["flake.nix"],
toml: ["pyproject.toml", "Cargo.toml", "taplo.toml"],
csharp: DOTNET_CSHARP_ROOT_MARKERS,
fsharp: DOTNET_FSHARP_ROOT_MARKERS
};
var TOOL_MARKERS_BY_RUNNER = {
ruff: ["ruff.toml", ".ruff.toml"],
oxlint: [".oxlintrc.json", "oxlint.config.js"],
"spellcheck/typos": ["_typos.toml", "typos.toml"],
yamllint: ["yamllint.yaml", "yamllint.yml"],
prettier: [".prettierignore"]
};
function rootMarkersForFile(filePath, runner) {
const kind = detectFileKind(path.resolve(filePath));
const languageMarkers = kind ? ROOT_MARKERS_BY_KIND[kind] ?? [] : [];
const toolMarkers = runner ? TOOL_MARKERS_BY_RUNNER[runner] ?? [] : [];
return toolMarkers.length ? [.../* @__PURE__ */ new Set([...languageMarkers, ...toolMarkers])] : languageMarkers;
}
function hasProjectMarker(projectRoot, marker) {
if (!marker.includes("*"))
return fs.existsSync(path.join(projectRoot, marker));
try {
return direntsHaveMarkerGlobMatch(fs.readdirSync(projectRoot, { withFileTypes: true }), marker);
} catch {
return false;
}
}
var languageProfileCache = new BoundedLruCache(32);
registerWorkspaceTopologyReset(() => languageProfileCache.clear());
function detectProjectLanguageProfile(projectRoot, sourceFiles) {
if (sourceFiles === void 0) {
const cached = languageProfileCache.get(normalizeMapKey(projectRoot));
if (cached)
return cached;
}
const result = computeProjectLanguageProfile(projectRoot, sourceFiles);
if (sourceFiles === void 0) {
languageProfileCache.set(normalizeMapKey(projectRoot), result);
}
return result;
}
function computeProjectLanguageProfile(projectRoot, sourceFiles) {
const present = Object.fromEntries(SUPPORTED_FILE_KINDS.map((kind) => [kind, false]));
const counts = {};
const configured = {};
for (const [kind, markers] of Object.entries(PROJECT_MARKERS_BY_KIND)) {
if (!markers)
continue;
for (const marker of markers) {
if (hasProjectMarker(projectRoot, marker)) {
present[kind] = true;
configured[kind] = true;
break;
}
}
}
let files = sourceFiles;
if (!files) {
try {
files = getSourceFiles(projectRoot, true);
} catch {
files = [];
}
}
for (const file of files) {
const kind = detectFileKind(file);
if (!kind)
continue;
present[kind] = true;
counts[kind] = (counts[kind] ?? 0) + 1;
}
const detectedKinds = SUPPORTED_FILE_KINDS.filter((kind) => present[kind]);
return {
present,
configured,
counts,
detectedKinds
};
}
function getDefaultStartupTools(profile) {
const tools = /* @__PURE__ */ new Set();
for (const kind of Object.keys(LANGUAGE_POLICY)) {
if (!profile.present[kind])
continue;
const defaults = LANGUAGE_POLICY[kind].startup?.defaults ?? [];
for (const tool of defaults) {
if (LANGUAGE_POLICY[kind].startup?.heavyScansRequireConfig && !profile.configured[kind]) {
continue;
}
tools.add(tool);
}
}
return [...tools];
}
function resolveLanguageRootForFile(filePath, workspaceRoot) {
const absoluteFilePath = path.resolve(filePath);
const startDir = path.dirname(absoluteFilePath);
const kind = detectFileKind(absoluteFilePath);
if (!kind)
return path.resolve(workspaceRoot);
const markers = rootMarkersForFile(absoluteFilePath);
if (!markers || markers.length === 0) {
return path.resolve(workspaceRoot);
}
const found = findNearestDirWithAnyBasename(startDir, markers);
if (!found)
return path.resolve(workspaceRoot);
const workspace = path.resolve(workspaceRoot);
const relative2 = path.relative(workspace, found);
if (relative2.startsWith("..") || path.isAbsolute(relative2)) {
return workspace;
}
return found;
}
var WARMUP_SOURCE_EXTS = new Set(SUPPORTED_FILE_KINDS.flatMap((kind) => KIND_EXTENSIONS[kind]));
var WARMUP_CODE_EXTS = new Set(SUPPORTED_FILE_KINDS.filter((kind) => CODE_KINDS.has(kind)).flatMap((kind) => KIND_EXTENSIONS[kind]));
var MAX_WARMUP_SOURCE_FILES = 2e3;
var MATCHED_FILES_CEILING_FACTOR = 10;
async function collectSourceFilesForWarmup(rootDir, maxFiles = MAX_WARMUP_SOURCE_FILES, yieldEvery = 100) {
const root = path.resolve(rootDir);
const ignoreMatcher = getProjectIgnoreMatcher(root);
await ignoreMatcher.ensureTrackedIndex();
const stack = [root];
const codeOut = [];
const nonCodeOut = [];
let matchedSeen = 0;
let processedSinceYield = 0;
walk: while (stack.length > 0) {
const current = stack.pop();
if (!current)
continue;
const entries = readDirEntriesSafe(current);
for (const entry of entries) {
const fullPath = path.join(current, entry.name);
if (entry.isDirectory()) {
if (!shouldRecurseIntoDir(entry, fullPath, {
ignoreMatcher,
followSymlinks: true
})) {
continue;
}
stack.push(fullPath);
} else if (entry.isFile()) {
const ext = path.extname(entry.name).toLowerCase();
if (!WARMUP_SOURCE_EXTS.has(ext))
continue;
if (ignoreMatcher.isIgnored(fullPath, false))
continue;
matchedSeen += 1;
const bucket = WARMUP_CODE_EXTS.has(ext) ? codeOut : nonCodeOut;
if (bucket.length < maxFiles)
bucket.push(fullPath);
if (codeOut.length >= maxFiles || matchedSeen >= MATCHED_FILES_CEILING_FACTOR * maxFiles) {
break walk;
}
}
if (++processedSinceYield % yieldEvery === 0) {
await new Promise((resolve2) => setImmediate(resolve2));
}
}
}
return [...codeOut, ...nonCodeOut];
}
async function detectProjectLanguageProfileAsync(projectRoot) {
const cached = languageProfileCache.get(normalizeMapKey(projectRoot));
if (cached)
return cached;
const files = await collectSourceFilesForWarmup(projectRoot);
const result = detectProjectLanguageProfile(projectRoot, files);
languageProfileCache.set(normalizeMapKey(projectRoot), result);
return result;
}
export {
getLspCapableKinds,
getPrimaryDispatchGroup,
canRunStartupHeavyScans,
SUPPORTED_FILE_KINDS,
rootMarkersForFile,
detectProjectLanguageProfile,
getDefaultStartupTools,
resolveLanguageRootForFile,
WARMUP_SOURCE_EXTS,
collectSourceFilesForWarmup,
detectProjectLanguageProfileAsync
};