@herbertgao/pi-extensions 2026.9.14 → 2026.9.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/THIRD_PARTY_NOTICES.md +28 -2
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -6
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +4 -4
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +2 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +16 -4
- package/node_modules/pi-jev-auto-mode/CHANGELOG.md +8 -0
- package/node_modules/pi-jev-auto-mode/README.md +7 -0
- package/node_modules/pi-jev-auto-mode/package.json +1 -1
- package/node_modules/pi-jev-auto-mode/src/extension.ts +21 -2
- package/node_modules/pi-jev-auto-mode/src/records.ts +68 -2
- package/node_modules/pi-jev-auto-mode/src/settings.ts +22 -0
- package/node_modules/pi-jev-auto-mode/src/ui.ts +10 -0
- package/node_modules/pi-lens/CHANGELOG.md +498 -0
- package/node_modules/pi-lens/README.md +45 -2
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +23 -7
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +187 -51
- package/node_modules/pi-lens/dist/clients/cache-manager.js +64 -5
- package/node_modules/pi-lens/dist/clients/child-unref.js +17 -8
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
- package/node_modules/pi-lens/dist/clients/config-locations.js +237 -9
- package/node_modules/pi-lens/dist/clients/config-resolve.js +7 -1
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +33 -3
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +87 -64
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -3
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +118 -1
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +23 -4
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +14 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +26 -28
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +38 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +32 -49
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +16 -14
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +25 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +28 -47
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +27 -22
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +31 -46
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +29 -46
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +91 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +16 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +38 -23
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +27 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +31 -21
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +34 -60
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +6 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +12 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +22 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +39 -16
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +27 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +18 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +6 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +6 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +11 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +10 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +11 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +12 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +30 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +23 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +15 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +12 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +27 -39
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +13 -3
- package/node_modules/pi-lens/dist/clients/effective-config.js +3 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +42 -11
- package/node_modules/pi-lens/dist/clients/formatters.js +49 -11
- package/node_modules/pi-lens/dist/clients/git-guard.js +247 -34
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +121 -25
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +186 -0
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +19 -6
- package/node_modules/pi-lens/dist/clients/installer/index.js +281 -32
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +8 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-config.js +66 -8
- package/node_modules/pi-lens/dist/clients/lsp/client.js +139 -11
- package/node_modules/pi-lens/dist/clients/lsp/config.js +9 -7
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +41 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +23 -2
- package/node_modules/pi-lens/dist/clients/lsp/server.js +106 -25
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +14 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +105 -16
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +232 -44
- package/node_modules/pi-lens/dist/clients/mcp/review.js +53 -7
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +11 -6
- package/node_modules/pi-lens/dist/clients/pipeline.js +121 -66
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +11 -3
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +34 -26
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +16 -10
- package/node_modules/pi-lens/dist/clients/ruff-client.js +31 -4
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +38 -4
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +16 -5
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +27 -12
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +447 -280
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +256 -65
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +84 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +119 -58
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +51 -0
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +112 -0
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +204 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +59 -0
- package/node_modules/pi-lens/dist/clients/widget-state.js +145 -14
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +57 -3
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -13
- package/node_modules/pi-lens/dist/index.js +5695 -3733
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +4 -0
- package/node_modules/pi-lens/dist/mcp/server.js +33 -21
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +43 -16
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +118 -7
- package/node_modules/pi-lens/docs/adr/0001-stale-advisory-live-arm.md +29 -0
- package/node_modules/pi-lens/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
- package/node_modules/pi-lens/docs/adr/0003-git-guard-latch-writer.md +203 -0
- package/node_modules/pi-lens/docs/adr/0004-disposition-policy-seam.md +38 -0
- package/node_modules/pi-lens/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
- package/node_modules/pi-lens/docs/adr/0006-derived-state-benchmark-first.md +31 -0
- package/node_modules/pi-lens/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
- package/node_modules/pi-lens/docs/adr/0008-turn-end-lane-interface.md +165 -0
- package/node_modules/pi-lens/docs/adr/0009-reported-path-attribution.md +114 -0
- package/node_modules/pi-lens/docs/adr/README.md +26 -0
- package/node_modules/pi-lens/docs/agent-tools.md +18 -1
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +4 -3
- package/node_modules/pi-lens/docs/configuration.md +55 -1
- package/node_modules/pi-lens/docs/engineering-principles.md +202 -0
- package/node_modules/pi-lens/docs/environment-variables.md +61 -4
- package/node_modules/pi-lens/docs/features.md +2 -2
- package/node_modules/pi-lens/docs/globalconfig.md +6 -1
- package/node_modules/pi-lens/docs/language-coverage.md +17 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +98 -31
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +57 -3
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +9 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +16 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +9 -0
- package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +2 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +21 -11
- package/node_modules/pi-lens/docs/settings.md +12 -3
- package/node_modules/pi-lens/docs/subagent-compat.md +1 -1
- package/node_modules/pi-lens/package.json +5 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
- package/node_modules/pi-lens/scripts/install-selftest.mjs +21 -17
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +29 -0
- package/node_modules/pi-mcp-adapter/OAUTH.md +369 -0
- package/node_modules/pi-mcp-adapter/README.md +12 -5
- package/node_modules/pi-mcp-adapter/config.ts +31 -8
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +1 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +33 -8
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +15 -2
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +27 -2
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/server-manager.js +6 -4
- package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +2 -2
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +10 -3
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +37 -20
- package/node_modules/pi-mcp-adapter/init.ts +14 -15
- package/node_modules/pi-mcp-adapter/json-schema-validator.ts +16 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +27 -2
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +1 -1
- package/node_modules/pi-mcp-adapter/package.json +3 -2
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -3
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +99 -6
- package/node_modules/pi-mcp-adapter/types.ts +2 -2
- package/node_modules/pi-mcp-adapter/ui-session.ts +60 -27
- package/node_modules/pi-mcp-adapter/utils.ts +11 -3
- package/node_modules/pi-multi-account/CHANGELOG.md +6 -0
- package/node_modules/pi-multi-account/README.md +15 -1
- package/node_modules/pi-multi-account/index.ts +295 -41
- package/node_modules/pi-multi-account/package.json +1 -1
- package/node_modules/pi-next-cue/.github/workflows/publish.yml +20 -0
- package/node_modules/pi-next-cue/LICENSE +21 -0
- package/node_modules/pi-next-cue/README.md +81 -0
- package/node_modules/pi-next-cue/index.ts +358 -0
- package/node_modules/pi-next-cue/package.json +28 -0
- package/package.json +10 -7
|
@@ -74,6 +74,14 @@ export class JscpdClient {
|
|
|
74
74
|
constructor(verbose = false) {
|
|
75
75
|
this.log = verbose ? createSubsystemLogger("jscpd") : () => { };
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Wait for scans already started by this client to settle. Test owners use
|
|
79
|
+
* this as the lifecycle barrier before removing fixture roots; the scan
|
|
80
|
+
* promise includes the jscpd child and its report-directory cleanup.
|
|
81
|
+
*/
|
|
82
|
+
async shutdown() {
|
|
83
|
+
await Promise.allSettled(this.inFlight.values());
|
|
84
|
+
}
|
|
77
85
|
/**
|
|
78
86
|
* Fast recursive source file presence check.
|
|
79
87
|
* Avoids running jscpd when repo has no relevant source files.
|
|
@@ -85,6 +85,12 @@ let recentPhases = [];
|
|
|
85
85
|
* zero-duration session-end summary of records already logged individually,
|
|
86
86
|
* not new work of its own.
|
|
87
87
|
*
|
|
88
|
+
* #3310: `lsp_empty_first_publish_held` is the same shape as the #1459
|
|
89
|
+
* scanner-coverage rows — a decision record written from inside a live
|
|
90
|
+
* `lsp_touch_file` wait whose `durationMs` is ANOTHER write's age (how long
|
|
91
|
+
* after `didOpen` the server's pre-index empty publish arrived), not its own
|
|
92
|
+
* work. The touch it fires inside owns any real stall.
|
|
93
|
+
*
|
|
88
94
|
* #2044: `test_runner_failed_target_state` is a zero-duration decision after a
|
|
89
95
|
* bounded filesystem probe. The surrounding turn-end test-selection phase owns
|
|
90
96
|
* any real work, so this row must not replace it in stall attribution.
|
|
@@ -104,6 +110,7 @@ const LAST_PHASE_EXCLUDED = new Set([
|
|
|
104
110
|
"availability_decision",
|
|
105
111
|
"finding_dead_path_drop",
|
|
106
112
|
"finding_stale_line_demote",
|
|
113
|
+
"finding_path_stat_budget_exhausted",
|
|
107
114
|
"lsp_scanner_coverage_gap",
|
|
108
115
|
"lsp_notify_resync_deferred",
|
|
109
116
|
"lsp_notify_write_late_landed",
|
|
@@ -112,6 +119,7 @@ const LAST_PHASE_EXCLUDED = new Set([
|
|
|
112
119
|
"path_attribution_verified_rollup",
|
|
113
120
|
"concurrent_session_bind_rollup",
|
|
114
121
|
"auxiliary_readiness",
|
|
122
|
+
"lsp_empty_first_publish_held",
|
|
115
123
|
]);
|
|
116
124
|
/**
|
|
117
125
|
* The last non-`loop_block` phase logged, or undefined if none yet. Carries its
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { resetIgnoredConfigWarnCache, warnIgnoredConfigOnce, } from "./config-warn.js";
|
|
2
|
+
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
2
3
|
import { errorClassName } from "./error-class.js";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import { assignFlagConfigSection, flagConfigSectionKeys, flagValueFromConfig, getLensFlagSpec, GLOBAL_NON_FLAG_CONFIG_SECTIONS, LENS_FLAGS, readFlagConfigValue, } from "./lens-flag-registry.js";
|
|
5
|
-
import { CANONICAL_GLOBAL_CONFIG_FILE, getPiLensGlobalConfigPath, GLOBAL_CONFIG_LOCATIONS, LEGACY_ROOT_LSP_KEYS, } from "./config-locations.js";
|
|
6
|
+
import { CANONICAL_GLOBAL_CONFIG_FILE, canonicalPathIdentity, getPiLensGlobalConfigPath, getProductionGlobalConfigResolution, GLOBAL_CONFIG_LOCATIONS, LEGACY_ROOT_LSP_KEYS, } from "./config-locations.js";
|
|
6
7
|
// Re-exported so this module's own, pre-existing import sites (`./lens-config.js`
|
|
7
8
|
// / `../lens-config.js`) keep working. The function itself now lives in
|
|
8
9
|
// `config-locations.ts` (#2426 review round 3, S1) — see the doc comment there.
|
|
9
|
-
export { getPiLensGlobalConfigPath } from "./config-locations.js";
|
|
10
|
+
export { getPiLensGlobalConfigPath, getProductionGlobalConfigResolution, isResolvedGlobalConfigPath, resolveGlobalConfigLocation, resetGlobalConfigLocationCache, } from "./config-locations.js";
|
|
10
11
|
import { ignoredRecordCollector, readConfigDocument, reportConfigReadFailure, reportPiLensConfigRecords, resolveOnePiLensConfigDocument, } from "./config-resolve.js";
|
|
11
12
|
import { findNestedProjectMutationValue, } from "./project-lens-config.js";
|
|
12
13
|
import { readToolConfig } from "./tool-config.js";
|
|
@@ -46,7 +47,64 @@ function asConfigObject(value) {
|
|
|
46
47
|
? value
|
|
47
48
|
: undefined;
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Report a probe-error RETENTION decision once (global-config-location PR,
|
|
52
|
+
* refs #2457; review H2 remedy B).
|
|
53
|
+
*
|
|
54
|
+
* Fired by `loadPiLensGlobalConfig` when it derived the path itself and the
|
|
55
|
+
* resolution retained a location whose existence probe THREW. The record
|
|
56
|
+
* names the retained path (subject) and the failed probe's error class;
|
|
57
|
+
* once per session via `recordDegradationOnce`. A successful
|
|
58
|
+
* non-canonical selection fires nothing (opt-in by file creation, visible
|
|
59
|
+
* via `pilens_effective_config`).
|
|
60
|
+
*/
|
|
61
|
+
function reportGlobalConfigProbeRetention() {
|
|
62
|
+
const resolution = getProductionGlobalConfigResolution();
|
|
63
|
+
if (resolution.source !== "legacy-default-unprobed" &&
|
|
64
|
+
resolution.source !== "pi-coding-agent-dir-unprobed") {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
recordDegradationOnce({
|
|
68
|
+
kind: "config-location-probe-failed",
|
|
69
|
+
subject: resolution.path,
|
|
70
|
+
reason: `existence probe for ${resolution.existsProbeFailed?.path} failed (${resolution.existsProbeFailed?.errorClassName}); the location is retained as the config source and its read failures report through PILENS_CFG_0001`,
|
|
71
|
+
metadata: { subsystem: "lens-config", configPath: resolution.path },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Report a lower-precedence global config that exists beside the winner. */
|
|
75
|
+
function reportGlobalConfigShadowing() {
|
|
76
|
+
const resolution = getProductionGlobalConfigResolution();
|
|
77
|
+
if (resolution.shadowedPath === undefined)
|
|
78
|
+
return;
|
|
79
|
+
const winningPath = canonicalPathIdentity(resolution.path);
|
|
80
|
+
const shadowedPath = canonicalPathIdentity(resolution.shadowedPath);
|
|
81
|
+
recordDegradationOnce({
|
|
82
|
+
kind: "config-location-shadowed",
|
|
83
|
+
subject: winningPath,
|
|
84
|
+
reason: `shadowed global config ${shadowedPath}; winning path is the record subject`,
|
|
85
|
+
metadata: {
|
|
86
|
+
subsystem: "lens-config",
|
|
87
|
+
configPath: winningPath,
|
|
88
|
+
shadowedPath,
|
|
89
|
+
},
|
|
90
|
+
code: "PILENS_CFG_0010",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export function loadPiLensGlobalConfig(configPath) {
|
|
94
|
+
// An unresolvable existence probe on the derived path is the one decision
|
|
95
|
+
// that must carry its own bounded record (#3251 review H2 remedy B): the
|
|
96
|
+
// resolution RETAINED the location it could not evaluate, the read below
|
|
97
|
+
// reports its own `config-ignored` row when it fails, and this record
|
|
98
|
+
// names the retention decision itself - the failed probe's path and error
|
|
99
|
+
// class - so "different settings silently applying" is impossible.
|
|
100
|
+
// Explicit-path callers stay silent (#2427 quiet contract); the default
|
|
101
|
+
// parameter is evaluated FIRST so the body can tell derived from explicit.
|
|
102
|
+
const derivedPath = configPath === undefined;
|
|
103
|
+
if (derivedPath) {
|
|
104
|
+
reportGlobalConfigProbeRetention();
|
|
105
|
+
reportGlobalConfigShadowing();
|
|
106
|
+
}
|
|
107
|
+
const resolvedPath = configPath ?? getPiLensGlobalConfigPath();
|
|
50
108
|
const location = globalCanonicalLocation();
|
|
51
109
|
// #2445: this used to be `JSON.parse(fs.readFileSync(...))` inside a bare
|
|
52
110
|
// `catch { return undefined; }`, so a malformed `~/.pi-lens/config.json`
|
|
@@ -58,12 +116,12 @@ export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath())
|
|
|
58
116
|
// `reportConfigReadFailure` derives the subsystem from the DOCUMENT so the
|
|
59
117
|
// LSP loader's report of this file lands under `lens-config` as well and the
|
|
60
118
|
// warn-once latch collapses the two into one honest notice.
|
|
61
|
-
const outcome = readConfigDocument(
|
|
119
|
+
const outcome = readConfigDocument(resolvedPath);
|
|
62
120
|
if (outcome.status === "missing")
|
|
63
121
|
return undefined;
|
|
64
122
|
if (outcome.status === "error") {
|
|
65
123
|
reportConfigReadFailure({
|
|
66
|
-
file:
|
|
124
|
+
file: resolvedPath,
|
|
67
125
|
location,
|
|
68
126
|
tier: "global",
|
|
69
127
|
error: outcome.error,
|
|
@@ -92,7 +150,7 @@ export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath())
|
|
|
92
150
|
// The flush is in a `finally` so a throw in the projection still delivers
|
|
93
151
|
// what was composed before it, on top of the whole-config record the catch
|
|
94
152
|
// adds.
|
|
95
|
-
const { note, records: notedRecords } = ignoredRecordCollector(
|
|
153
|
+
const { note, records: notedRecords } = ignoredRecordCollector(resolvedPath, "global");
|
|
96
154
|
try {
|
|
97
155
|
const parsed = outcome.value;
|
|
98
156
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -110,7 +168,7 @@ export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath())
|
|
|
110
168
|
// function still decides what a bad value is called.
|
|
111
169
|
const document = {
|
|
112
170
|
tier: "global",
|
|
113
|
-
file:
|
|
171
|
+
file: resolvedPath,
|
|
114
172
|
location,
|
|
115
173
|
value: parsed,
|
|
116
174
|
};
|
|
@@ -269,7 +327,7 @@ export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath())
|
|
|
269
327
|
// calling the same leaf `config-warn.ts` itself delegates to keeps every
|
|
270
328
|
// caller of this guarantee, `config-core/` included, on ONE
|
|
271
329
|
// implementation rather than two that happen to agree today.
|
|
272
|
-
warnInvalidGlobalConfigOnce(
|
|
330
|
+
warnInvalidGlobalConfigOnce(resolvedPath, `global config could not be interpreted (${errorClassName(error)}); configuration ignored`, "PILENS_CFG_0008");
|
|
273
331
|
return undefined;
|
|
274
332
|
}
|
|
275
333
|
finally {
|
|
@@ -32,7 +32,8 @@ import { hashDiagnosticContent, } from "./diagnostic-binding.js";
|
|
|
32
32
|
import { applyWorkspaceEdit, normalizeWorkspaceEditToUtf16 } from "./edits.js";
|
|
33
33
|
import { normalizeMapKey, uriToPath } from "./path-utils.js";
|
|
34
34
|
import { ADVERTISED_POSITION_ENCODINGS, convertCharacterOffset, lineTextAt, negotiatePositionEncoding, } from "./position-encoding.js";
|
|
35
|
-
import { negotiateSyncKind, TEXT_DOCUMENT_SYNC_KIND_FULL, TEXT_DOCUMENT_SYNC_KIND_INCREMENTAL, } from "./sync-kind.js";
|
|
35
|
+
import { negotiateSaveOptions, negotiateSyncKind, TEXT_DOCUMENT_SYNC_KIND_FULL, TEXT_DOCUMENT_SYNC_KIND_INCREMENTAL, } from "./sync-kind.js";
|
|
36
|
+
import { exceedsLspSyncLimits } from "./content-limits.js";
|
|
36
37
|
import { probeTsserverProjectIdentity } from "./tsserver-sync.js";
|
|
37
38
|
import { getStrategy } from "./wait-policy/index.js";
|
|
38
39
|
import { WatchedFilesQueue } from "./watch-queue.js";
|
|
@@ -1162,13 +1163,13 @@ export function applyDynamicCapabilities(state) {
|
|
|
1162
1163
|
}
|
|
1163
1164
|
export function resolveConfigurationSection(initialization, section) {
|
|
1164
1165
|
if (!initialization)
|
|
1165
|
-
return
|
|
1166
|
+
return {};
|
|
1166
1167
|
if (!section)
|
|
1167
1168
|
return initialization;
|
|
1168
1169
|
let cur = initialization;
|
|
1169
1170
|
for (const part of section.split(".")) {
|
|
1170
1171
|
if (typeof cur !== "object" || cur === null || !Object.hasOwn(cur, part)) {
|
|
1171
|
-
return
|
|
1172
|
+
return {};
|
|
1172
1173
|
}
|
|
1173
1174
|
cur = cur[part];
|
|
1174
1175
|
}
|
|
@@ -1326,6 +1327,60 @@ export function setupIncomingHandlers(state, initialization, onDiagnosticsPublis
|
|
|
1326
1327
|
const currentVersion = state.documentVersions.get(normalizedPath);
|
|
1327
1328
|
return currentVersion !== undefined && docVersion < currentVersion;
|
|
1328
1329
|
};
|
|
1330
|
+
// #3310: HOLD the empty first publish of an asynchronously-indexing
|
|
1331
|
+
// server. Measured (docs/lsp-capability-matrix.md's `first-publish`
|
|
1332
|
+
// column): intelephense answers `didOpen` with `[]` while its
|
|
1333
|
+
// whole-workspace index builds and publishes the real set once
|
|
1334
|
+
// indexing ends, so letting that first publish through cached an empty
|
|
1335
|
+
// set, bumped the publication stamp and emitted — which resolved the
|
|
1336
|
+
// push wait AND satisfied the "answered" evidence check in
|
|
1337
|
+
// `clients/lsp/index.ts`, rendering a file with an error as "confirmed
|
|
1338
|
+
// clean". A timeout is not a false clean; an empty pre-index publish
|
|
1339
|
+
// must not become one either.
|
|
1340
|
+
//
|
|
1341
|
+
// Held means: not cached, no version bump, no emit — so it can neither
|
|
1342
|
+
// resolve a wait nor count as evidence. Nothing is scheduled and
|
|
1343
|
+
// nothing is awaited: the server's OWN next publish releases the hold
|
|
1344
|
+
// (it is no longer a first publish), and the existing per-server budget
|
|
1345
|
+
// stays the only bound, so there is no new timer, listener or
|
|
1346
|
+
// cancellation path. A genuinely clean file still gets an affirmative
|
|
1347
|
+
// clean, because the measured server re-publishes `[]` at the end of
|
|
1348
|
+
// indexing.
|
|
1349
|
+
//
|
|
1350
|
+
// Three conditions, each load-bearing:
|
|
1351
|
+
// - the MEASURED class (never every push server): a Tier 2/2* server
|
|
1352
|
+
// that publishes `[]` once for a clean file keeps resolving the wait
|
|
1353
|
+
// on it, with no added latency (#3310 AC2);
|
|
1354
|
+
// - one-shot per client session: the cold index builds once, so a warm
|
|
1355
|
+
// touch — or a class server that never re-publishes — pays nothing;
|
|
1356
|
+
// - FIRST publication for this document: no cached push AND no pending
|
|
1357
|
+
// debounce timer. An empty publish that CLEARS an earlier non-empty
|
|
1358
|
+
// one (a fix landing) is never held, in either arrival order.
|
|
1359
|
+
if (strategy.emptyFirstPublish === "indexing" &&
|
|
1360
|
+
!state.emptyFirstPublishHoldSpent &&
|
|
1361
|
+
newDiags.length === 0 &&
|
|
1362
|
+
!state.pushDiagnostics.has(normalizedPath) &&
|
|
1363
|
+
!state.pendingDiagnostics.has(normalizedPath)) {
|
|
1364
|
+
state.emptyFirstPublishHoldSpent = true;
|
|
1365
|
+
// Bounded by construction: one record per client session, because
|
|
1366
|
+
// the hold itself is one-shot.
|
|
1367
|
+
logLatency({
|
|
1368
|
+
type: "phase",
|
|
1369
|
+
phase: "lsp_empty_first_publish_held",
|
|
1370
|
+
filePath: normalizedPath,
|
|
1371
|
+
durationMs: Math.max(0, publishReceivedAt -
|
|
1372
|
+
(state.documentOpenedAt.get(normalizedPath) ?? publishReceivedAt)),
|
|
1373
|
+
metadata: {
|
|
1374
|
+
serverId: state.serverId,
|
|
1375
|
+
emptyFirstPublish: strategy.emptyFirstPublish,
|
|
1376
|
+
// `pubVersion`, the `[lsp-pub]` trace's own field name for the
|
|
1377
|
+
// publish's LSP document version — not a lifecycle identity, and
|
|
1378
|
+
// not the retired glossary spelling (AGENTS.md "generation").
|
|
1379
|
+
pubVersion: docVersion ?? "push-unversioned",
|
|
1380
|
+
},
|
|
1381
|
+
});
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1329
1384
|
// Seed on first push for servers whose first push is known complete.
|
|
1330
1385
|
// Bypasses the debounce timer entirely — resolves waiting promises immediately.
|
|
1331
1386
|
if (strategy.seedFirstPush &&
|
|
@@ -1477,7 +1532,9 @@ export function setupIncomingHandlers(state, initialization, onDiagnosticsPublis
|
|
|
1477
1532
|
// dot-path into the server's config, e.g. "scan.jobs") — not a fixed
|
|
1478
1533
|
// single-element array duplicating the whole blob for every item. An item
|
|
1479
1534
|
// with no `section` gets the whole blob (that's what "no section" means
|
|
1480
|
-
// per spec); an unresolvable section gets `
|
|
1535
|
+
// per spec); an unresolvable section gets `{}` (never `null`: strict consumers such as
|
|
1536
|
+
// vscode-css-language-server and @prisma/language-server throw or exit on
|
|
1537
|
+
// null), never the whole blob.
|
|
1481
1538
|
state.connection.onRequest("workspace/configuration", async (params) => {
|
|
1482
1539
|
const items = params?.items ?? [];
|
|
1483
1540
|
return items.map((item) => resolveConfigurationSection(initialization, item?.section));
|
|
@@ -2455,7 +2512,64 @@ export function handleNotifyExternalChange(state, filePath, type) {
|
|
|
2455
2512
|
const uri = state.openDocumentUris?.get(normalizedPath) ?? pathToFileURL(filePath).href;
|
|
2456
2513
|
state.watchQueue.enqueue(uri, type);
|
|
2457
2514
|
}
|
|
2458
|
-
|
|
2515
|
+
/**
|
|
2516
|
+
* #3405: tell the server the document it just received is the file's saved
|
|
2517
|
+
* on-disk state.
|
|
2518
|
+
*
|
|
2519
|
+
* pi-lens has no unsaved-buffer concept — every notification it sends carries
|
|
2520
|
+
* bytes read from disk — so a save-triggered server is the one class of server
|
|
2521
|
+
* this client could never reach: `CLIENT_CAPABILITIES` has advertised
|
|
2522
|
+
* `synchronization.didSave` since #278 and no caller emitted one, so Expert
|
|
2523
|
+
* (whose ONLY whole-project recompile trigger is didSave —
|
|
2524
|
+
* `expert-lsp/expert@6bbad8c` `apps/expert/lib/expert/state.ex:243-257`)
|
|
2525
|
+
* published nothing for an edit made through pi-lens.
|
|
2526
|
+
*
|
|
2527
|
+
* Sent ONLY when the caller declared this touch a save (`saved`) AND the server
|
|
2528
|
+
* declared `textDocumentSync.save`; `includeText` decides whether the text
|
|
2529
|
+
* rides along. Both gates fail closed: an undeclared notification is the #278
|
|
2530
|
+
* class of hazard, and a save claimed for content whose didOpen/didChange never
|
|
2531
|
+
* left the process would tell the server to diagnose bytes it does not have —
|
|
2532
|
+
* so every call site below sends this only after its own content notification
|
|
2533
|
+
* returned `true` from `safeSendNotification`, and never for a document this
|
|
2534
|
+
* client has not opened.
|
|
2535
|
+
*/
|
|
2536
|
+
async function sendDidSave(state, uri, content) {
|
|
2537
|
+
const save = state.saveOptions;
|
|
2538
|
+
if (!save)
|
|
2539
|
+
return;
|
|
2540
|
+
if (!isClientAlive(state))
|
|
2541
|
+
return;
|
|
2542
|
+
// #3405 r2 (M3406-1): `text` is a SECOND full copy of bytes the server was
|
|
2543
|
+
// just handed, so it is the one payload here that can be file-sized. The
|
|
2544
|
+
// explicit `lsp_diagnostics` writer reads whole files with no bound of its
|
|
2545
|
+
// own, so without this the same 32MB string would be framed twice. Bound it
|
|
2546
|
+
// at the shared seam both writers pass through rather than at either caller.
|
|
2547
|
+
//
|
|
2548
|
+
// Omitting `text` is what the contract tolerates, and it is not a lost save:
|
|
2549
|
+
// `DidSaveTextDocumentParams.text` is OPTIONAL ("Optional the content when
|
|
2550
|
+
// saved") in the spec, FsAutoComplete — the one `includeText: true` server in
|
|
2551
|
+
// the registry — types it `string option` and, given `None`, keeps the source
|
|
2552
|
+
// it already holds and still runs its re-check
|
|
2553
|
+
// (ionide/FsAutoComplete@85886b1, AdaptiveServerState.fs lines 2649-2673),
|
|
2554
|
+
// and pi-lens only ever sends this after a landed didOpen/didChange for the
|
|
2555
|
+
// same document, so the server has these bytes either way. Skipping the save
|
|
2556
|
+
// outright would instead drop the diagnose trigger this PR exists to restore.
|
|
2557
|
+
const limit = save.includeText
|
|
2558
|
+
? exceedsLspSyncLimits(content)
|
|
2559
|
+
: { tooLarge: false, reason: "" };
|
|
2560
|
+
if (limit.tooLarge) {
|
|
2561
|
+
recordDegradationOnce({
|
|
2562
|
+
kind: "lsp-did-save-text-omitted",
|
|
2563
|
+
subject: state.serverId,
|
|
2564
|
+
reason: limit.reason,
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
await safeSendNotification(state.connection, "textDocument/didSave", {
|
|
2568
|
+
textDocument: { uri },
|
|
2569
|
+
...(save.includeText && !limit.tooLarge ? { text: content } : {}),
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
async function handleNotifyOpenOnce(state, filePath, content, languageId, preserveDiagnostics = false, silent = false, coalescedCount = 0, saved = false) {
|
|
2459
2573
|
if (!isClientAlive(state))
|
|
2460
2574
|
return;
|
|
2461
2575
|
const normalizedPath = normalizeMapKey(filePath);
|
|
@@ -2506,6 +2620,8 @@ async function handleNotifyOpenOnce(state, filePath, content, languageId, preser
|
|
|
2506
2620
|
recordSentContent(state, normalizedPath, version, content, coalescedCount);
|
|
2507
2621
|
state.openDocuments.add(normalizedPath);
|
|
2508
2622
|
state.openDocumentUris?.set(normalizedPath, uri);
|
|
2623
|
+
if (saved && reopenSent)
|
|
2624
|
+
await sendDidSave(state, uri, content);
|
|
2509
2625
|
return;
|
|
2510
2626
|
}
|
|
2511
2627
|
const changeSent = await safeSendNotification(state.connection, "textDocument/didChange", {
|
|
@@ -2514,6 +2630,8 @@ async function handleNotifyOpenOnce(state, filePath, content, languageId, preser
|
|
|
2514
2630
|
});
|
|
2515
2631
|
if (changeSent)
|
|
2516
2632
|
recordSentContent(state, normalizedPath, version, content, coalescedCount);
|
|
2633
|
+
if (saved && changeSent)
|
|
2634
|
+
await sendDidSave(state, uri, content);
|
|
2517
2635
|
return;
|
|
2518
2636
|
}
|
|
2519
2637
|
state.pendingOpens.add(normalizedPath);
|
|
@@ -2553,6 +2671,8 @@ async function handleNotifyOpenOnce(state, filePath, content, languageId, preser
|
|
|
2553
2671
|
state.openDocuments.add(normalizedPath);
|
|
2554
2672
|
state.closedDocuments?.delete(normalizedPath);
|
|
2555
2673
|
state.openDocumentUris?.set(normalizedPath, uri);
|
|
2674
|
+
if (saved && openSent)
|
|
2675
|
+
await sendDidSave(state, uri, content);
|
|
2556
2676
|
// Telemetry is deliberately detached after didOpen succeeds.
|
|
2557
2677
|
// #1412 H1: routed through runReadOnlyServerCommand, NOT runServerCommand —
|
|
2558
2678
|
// the probe must never open the serverEditsAllowed/activeMutationContext
|
|
@@ -2580,7 +2700,7 @@ async function handleNotifyOpenOnce(state, filePath, content, languageId, preser
|
|
|
2580
2700
|
* for notifications, so dropping a started write could violate didOpen before
|
|
2581
2701
|
* didChange ordering or leave a partial message in the pipe.
|
|
2582
2702
|
*/
|
|
2583
|
-
function enqueueDocumentNotify(state, normalizedPath, run) {
|
|
2703
|
+
function enqueueDocumentNotify(state, normalizedPath, run, saved = false) {
|
|
2584
2704
|
let queue = state.notifyChangeQueues.get(normalizedPath);
|
|
2585
2705
|
if (!queue) {
|
|
2586
2706
|
queue = { running: false };
|
|
@@ -2597,6 +2717,7 @@ function enqueueDocumentNotify(state, normalizedPath, run) {
|
|
|
2597
2717
|
run,
|
|
2598
2718
|
waiters,
|
|
2599
2719
|
coalescedCount: (previous?.coalescedCount ?? 0) + (previous ? 1 : 0),
|
|
2720
|
+
saved: saved || previous?.saved === true,
|
|
2600
2721
|
};
|
|
2601
2722
|
if (queue.running)
|
|
2602
2723
|
return;
|
|
@@ -2611,7 +2732,7 @@ function enqueueDocumentNotify(state, normalizedPath, run) {
|
|
|
2611
2732
|
break;
|
|
2612
2733
|
queue.pending = undefined;
|
|
2613
2734
|
try {
|
|
2614
|
-
await next.run(next.coalescedCount);
|
|
2735
|
+
await next.run(next.coalescedCount, next.saved);
|
|
2615
2736
|
for (const waiter of next.waiters)
|
|
2616
2737
|
waiter.resolve();
|
|
2617
2738
|
}
|
|
@@ -2640,11 +2761,11 @@ function cancelDocumentNotifyQueues(state) {
|
|
|
2640
2761
|
}
|
|
2641
2762
|
state.notifyChangeQueues.clear();
|
|
2642
2763
|
}
|
|
2643
|
-
export function handleNotifyOpen(state, filePath, content, languageId, preserveDiagnostics = false, silent = false) {
|
|
2764
|
+
export function handleNotifyOpen(state, filePath, content, languageId, preserveDiagnostics = false, silent = false, saved = false) {
|
|
2644
2765
|
if (!isClientAlive(state))
|
|
2645
2766
|
return Promise.resolve();
|
|
2646
2767
|
const normalizedPath = normalizeMapKey(filePath);
|
|
2647
|
-
return enqueueDocumentNotify(state, normalizedPath, (coalescedCount) => handleNotifyOpenOnce(state, filePath, content, languageId, preserveDiagnostics, silent, coalescedCount));
|
|
2768
|
+
return enqueueDocumentNotify(state, normalizedPath, (coalescedCount, queuedSaved) => handleNotifyOpenOnce(state, filePath, content, languageId, preserveDiagnostics, silent, coalescedCount, queuedSaved), saved);
|
|
2648
2769
|
}
|
|
2649
2770
|
async function handleNotifyChangeOnce(state, filePath, content, normalizedPath, coalescedCount = 0) {
|
|
2650
2771
|
if (!isClientAlive(state))
|
|
@@ -2691,6 +2812,10 @@ export function handleNotifyChange(state, filePath, content) {
|
|
|
2691
2812
|
if (!isClientAlive(state))
|
|
2692
2813
|
return Promise.resolve();
|
|
2693
2814
|
const normalizedPath = normalizeMapKey(filePath);
|
|
2815
|
+
// #3405: no `saved` argument — `LSPService.updateFile` is this path's only
|
|
2816
|
+
// entry point and no caller declares a save through it, so a change never
|
|
2817
|
+
// originates one. A save intent inherited from a superseded open entry is
|
|
2818
|
+
// dropped here by construction rather than sent after a bare didChange.
|
|
2694
2819
|
return enqueueDocumentNotify(state, normalizedPath, (coalescedCount) => handleNotifyChangeOnce(state, filePath, content, normalizedPath, coalescedCount));
|
|
2695
2820
|
}
|
|
2696
2821
|
/** Close a document through the same lifecycle path exposed by the client. */
|
|
@@ -3643,6 +3768,8 @@ export async function createLSPClient(options) {
|
|
|
3643
3768
|
pullRequestSequences: new Map(),
|
|
3644
3769
|
workspacePullResultCache: new Map(),
|
|
3645
3770
|
openDocuments: new Set(),
|
|
3771
|
+
// #3310: one-shot, per client session.
|
|
3772
|
+
emptyFirstPublishHoldSpent: false,
|
|
3646
3773
|
closedDocuments: new Set(),
|
|
3647
3774
|
openDocumentUris: new Map(),
|
|
3648
3775
|
pendingOpens: new Set(),
|
|
@@ -3798,6 +3925,7 @@ export async function createLSPClient(options) {
|
|
|
3798
3925
|
}
|
|
3799
3926
|
state.positionEncoding = negotiatePositionEncoding(initResult?.capabilities);
|
|
3800
3927
|
state.syncKind = negotiateSyncKind(initResult?.capabilities);
|
|
3928
|
+
state.saveOptions = negotiateSaveOptions(initResult?.capabilities);
|
|
3801
3929
|
state.rawCapabilityKeys = Object.keys(initResult?.capabilities ??
|
|
3802
3930
|
{}).sort((a, b) => a.localeCompare(b));
|
|
3803
3931
|
for (const cmd of detectExecuteCommands(initResult)) {
|
|
@@ -3835,8 +3963,8 @@ export async function createLSPClient(options) {
|
|
|
3835
3963
|
/** #2358: the OS pid of the live server process (see interface doc). */
|
|
3836
3964
|
getProcessPid: () => lspProcess.pid,
|
|
3837
3965
|
notify: {
|
|
3838
|
-
async open(filePath, content, languageId, preserveDiagnostics, silent) {
|
|
3839
|
-
return handleNotifyOpen(state, filePath, content, languageId, preserveDiagnostics, silent);
|
|
3966
|
+
async open(filePath, content, languageId, preserveDiagnostics, silent, saved) {
|
|
3967
|
+
return handleNotifyOpen(state, filePath, content, languageId, preserveDiagnostics, silent, saved);
|
|
3840
3968
|
},
|
|
3841
3969
|
async change(filePath, content) {
|
|
3842
3970
|
return handleNotifyChange(state, filePath, content);
|
|
@@ -261,13 +261,15 @@ export async function loadLSPConfig(cwd, homeDir = os.homedir(), options = {}) {
|
|
|
261
261
|
const resolution = resolvePiLensConfig({
|
|
262
262
|
cwd,
|
|
263
263
|
globalDir: getGlobalPiLensDir(),
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
|
|
264
|
+
// The global tier reads the PRODUCTION resolution (global-config-location
|
|
265
|
+
// PR, refs #2457): which file supplies it is an env fact (PI_LENS_CONFIG_PATH,
|
|
266
|
+
// then a legacy default, then PI_CODING_AGENT_DIR, then the
|
|
267
|
+
// PI_LENS_HOME-relocated canonical default), not a property of the
|
|
268
|
+
// `$HOME` this call's project walk is ceiling-bounded by. `homeDir`
|
|
269
|
+
// stays threaded below for the walk and `globalDir`; a test redirecting
|
|
270
|
+
// the global tier does it through the env, the same way production is
|
|
271
|
+
// controlled.
|
|
272
|
+
globalConfigPath: getPiLensGlobalConfigPath(),
|
|
271
273
|
homeDir,
|
|
272
274
|
// The subsystem comes from the failing DOCUMENT, not from this loader
|
|
273
275
|
// (#2445). This resolution opens `~/.pi-lens/config.json` and
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one byte/line bound on document content pi-lens hands a language server.
|
|
3
|
+
*
|
|
4
|
+
* #3405 r2 (M3406-1): this predicate used to exist twice — as a private
|
|
5
|
+
* `exceedsLspSyncLimits` in `clients/pipeline.ts` and again inline in
|
|
6
|
+
* `clients/dispatch/runners/lsp.ts` — so a THIRD writer could be added that
|
|
7
|
+
* simply had neither. That is what happened: `tools/lsp-diagnostics.ts` reads a
|
|
8
|
+
* whole file with `fs.readFileSync` and touches with it, and once the touch
|
|
9
|
+
* could also carry `textDocument/didSave.text` (`includeText`), the same
|
|
10
|
+
* unbounded string was serialized into a second JSON-RPC frame. Both callers now
|
|
11
|
+
* ask this module, and so does the didSave payload seam, so the bound is one
|
|
12
|
+
* constant pair in one place rather than a convention each new writer must
|
|
13
|
+
* remember.
|
|
14
|
+
*
|
|
15
|
+
* The two established callers act on it by skipping their whole LSP step — a
|
|
16
|
+
* file this large is not synced at all. That behaviour is deliberately NOT
|
|
17
|
+
* folded in here: what a caller does at the bound is the caller's policy, and
|
|
18
|
+
* `sendDidSave`'s is different (it drops the redundant `text` and still sends
|
|
19
|
+
* the save, because the server already received these bytes in the didOpen or
|
|
20
|
+
* didChange this save follows).
|
|
21
|
+
*/
|
|
22
|
+
import { RUNTIME_CONFIG } from "../runtime-config.js";
|
|
23
|
+
const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
|
|
24
|
+
const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
|
|
25
|
+
export function exceedsLspSyncLimits(content) {
|
|
26
|
+
const sizeBytes = Buffer.byteLength(content, "utf-8");
|
|
27
|
+
if (sizeBytes > LSP_MAX_FILE_BYTES) {
|
|
28
|
+
return {
|
|
29
|
+
tooLarge: true,
|
|
30
|
+
reason: `${sizeBytes} bytes > ${LSP_MAX_FILE_BYTES} limit`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const lineCount = content.split("\n").length;
|
|
34
|
+
if (lineCount > LSP_MAX_FILE_LINES) {
|
|
35
|
+
return {
|
|
36
|
+
tooLarge: true,
|
|
37
|
+
reason: `${lineCount} lines > ${LSP_MAX_FILE_LINES} limit`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { tooLarge: false, reason: "" };
|
|
41
|
+
}
|
|
@@ -3792,7 +3792,7 @@ export class LSPService {
|
|
|
3792
3792
|
// (or any synchronous throw) still reads as a rejected write rather
|
|
3793
3793
|
// than rejecting the whole per-file `Promise.all`.
|
|
3794
3794
|
const writePromise = entry.client.notify
|
|
3795
|
-
.open(filePath, content, languageId, undefined, silent)
|
|
3795
|
+
.open(filePath, content, languageId, undefined, silent, options.saved === true)
|
|
3796
3796
|
.then(() => true);
|
|
3797
3797
|
// #1714: the document is now in this auxiliary's input queue,
|
|
3798
3798
|
// whether or not the write settles inside our budget. Counted here
|
|
@@ -68,9 +68,30 @@ function splitPathEntries(value, delimiter) {
|
|
|
68
68
|
.map((entry) => entry.trim())
|
|
69
69
|
.filter((entry) => entry.length > 0);
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Dedupe key for one PATH entry, under `platform`'s rules.
|
|
73
|
+
*
|
|
74
|
+
* #1193 P3: the body used the HOST-DEFAULT `path.normalize` inside a branch
|
|
75
|
+
* already committed to `platform` (AGENTS.md defect shape 2), so on the ubuntu
|
|
76
|
+
* lane the win32 arm lowercased without folding separators or dot segments and
|
|
77
|
+
* `C:\Tools` / `C:/Tools` / `C:\x\..\Tools` were three keys for one directory.
|
|
78
|
+
* Each arm now parses with its own `path` flavor. That is the whole fix: a
|
|
79
|
+
* `toPosix` call on the win32 arm would be INERT — `win32.normalize` already
|
|
80
|
+
* answers backslashes for either input separator (measured: adding it left
|
|
81
|
+
* every case green) — and the POSIX arm must NOT fold separators at all,
|
|
82
|
+
* because a backslash is a legal POSIX filename character and `/opt/a\b` and
|
|
83
|
+
* `/opt/a/b` are two directories there.
|
|
84
|
+
*
|
|
85
|
+
* Ephemeral by construction: the key exists only inside one
|
|
86
|
+
* `combinePathValuesForPlatform` call's `seen` set — the ORIGINAL entry string
|
|
87
|
+
* is what survives into the joined PATH — so it never reaches a persisted or
|
|
88
|
+
* cross-process record and `normalizeEphemeralMapKey`'s `process.platform` read
|
|
89
|
+
* would defeat the explicit-`platform` seam these arms exist for.
|
|
90
|
+
*/
|
|
71
91
|
function normalizePathEntry(entry, platform) {
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
if (platform !== "win32")
|
|
93
|
+
return path.posix.normalize(entry);
|
|
94
|
+
return path.win32.normalize(entry).toLowerCase();
|
|
74
95
|
}
|
|
75
96
|
export function combinePathValuesForPlatform(values, platform = process.platform) {
|
|
76
97
|
const unique = [];
|