@moritzwilksch/diffle 0.2.5 → 0.2.7
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 +5 -4
- package/dist/client/assets/{angular-html-40_PP5IN.js → angular-html-DosOYpNl.js} +1 -1
- package/dist/client/assets/{angular-ts-Cg4RjOSa.js → angular-ts-DkqBGDar.js} +1 -1
- package/dist/client/assets/{apl-DF-eBnzw.js → apl-DhBUfBJf.js} +1 -1
- package/dist/client/assets/{astro-BOvFneDi.js → astro-C9ZTzAue.js} +1 -1
- package/dist/client/assets/{blade-Vanj6sax.js → blade-B1PWKc0N.js} +1 -1
- package/dist/client/assets/{c-DRaVis1k.js → c-DBA56zb-.js} +1 -1
- package/dist/client/assets/{chapel-DmJmjyke.js → chapel-ChRt-4dY.js} +1 -1
- package/dist/client/assets/{cobol-CZijWbs8.js → cobol-B4Ub-iW1.js} +1 -1
- package/dist/client/assets/{coffee-C7ADU3ov.js → coffee-x1BuBd_Q.js} +1 -1
- package/dist/client/assets/{cpp-CUnyhPxl.js → cpp-w_0aDvpB.js} +1 -1
- package/dist/client/assets/{crystal-CtVeCajj.js → crystal-ClQ0qP1s.js} +1 -1
- package/dist/client/assets/{css-Bgit8wl7.js → css-DC2faGEc.js} +1 -1
- package/dist/client/assets/{edge-Co1HUhdJ.js → edge-DmJBQ2gv.js} +1 -1
- package/dist/client/assets/{elixir-fkbKJn63.js → elixir-DCLVXjiT.js} +1 -1
- package/dist/client/assets/{elm-Uv71dj5J.js → elm-DlbDR8QF.js} +1 -1
- package/dist/client/assets/{erb-DDWRP60u.js → erb-DMRo3oW4.js} +1 -1
- package/dist/client/assets/{git-rebase-CQZTa8sS.js → git-rebase-DWIcl-q4.js} +1 -1
- package/dist/client/assets/{glimmer-js-DTm3z0uF.js → glimmer-js-DUaEeoFR.js} +1 -1
- package/dist/client/assets/{glimmer-ts-BVwvohFz.js → glimmer-ts-6tsiEBq-.js} +1 -1
- package/dist/client/assets/{glsl-Vqc6iypF.js → glsl-DdOwn0FP.js} +1 -1
- package/dist/client/assets/{graphql-C_PazajP.js → graphql-IJK_VTno.js} +1 -1
- package/dist/client/assets/{hack-KQ3tbVQ0.js → hack-DifbMrOT.js} +1 -1
- package/dist/client/assets/{haml-DZsm7DhW.js → haml-DeQ6UFH5.js} +1 -1
- package/dist/client/assets/{handlebars-BeI0Z9Ge.js → handlebars-BKpjBAcW.js} +1 -1
- package/dist/client/assets/{html-CtB5Q44Y.js → html-CTtX_fVs.js} +1 -1
- package/dist/client/assets/{html-derivative-WXvMfLLj.js → html-derivative-C6nfxO2F.js} +1 -1
- package/dist/client/assets/{http-C6pj3MSd.js → http-eQW7tfW5.js} +1 -1
- package/dist/client/assets/{hurl-BrHyymJJ.js → hurl-DVJuWXn_.js} +1 -1
- package/dist/client/assets/index-CqE8hk_s.js +1833 -0
- package/dist/client/assets/index-DZQ9igGc.css +2 -0
- package/dist/client/assets/{java-CPaOvu3h.js → java-ClwPIFd_.js} +1 -1
- package/dist/client/assets/{javascript-BPRT6jkl.js → javascript-D-3TtS2j.js} +1 -1
- package/dist/client/assets/{jinja-DTGLaj2c.js → jinja-B-6OnCvz.js} +1 -1
- package/dist/client/assets/{jison-DA5wRqQC.js → jison-BmwL1UCC.js} +1 -1
- package/dist/client/assets/{json-ED0VVZPE.js → json-D8CIZ6_Z.js} +1 -1
- package/dist/client/assets/{jsx-ssUbs28C.js → jsx-hHZI2uuq.js} +1 -1
- package/dist/client/assets/{julia-DLNyC4Uk.js → julia-CePjwP87.js} +1 -1
- package/dist/client/assets/{just-R9Z5Mjrx.js → just-BPRk2cx_.js} +1 -1
- package/dist/client/assets/{latex-BXG3aIes.js → latex-lrY1zFXa.js} +1 -1
- package/dist/client/assets/{liquid-CdepfROv.js → liquid-5ePjgMO9.js} +1 -1
- package/dist/client/assets/{lua-xbhHkSeE.js → lua-1rnc87r4.js} +1 -1
- package/dist/client/assets/{marko-BmP-9r68.js → marko-IKwBj9dr.js} +1 -1
- package/dist/client/assets/{mdc-URCCWXDZ.js → mdc-o7rCfvqw.js} +1 -1
- package/dist/client/assets/{nginx-CNVoVwWe.js → nginx-BsG6mAEE.js} +1 -1
- package/dist/client/assets/{nim-DcrAx62m.js → nim-v9Re54Nf.js} +1 -1
- package/dist/client/assets/{org-Cve18kq6.js → org-QPi2LyaR.js} +1 -1
- package/dist/client/assets/{perl-CNeZNpSH.js → perl-Bp-K4EZR.js} +1 -1
- package/dist/client/assets/{php-B_yAcPWV.js → php-9fs1O83G.js} +1 -1
- package/dist/client/assets/{pug-KzcMA0F1.js → pug-BObrWPFa.js} +1 -1
- package/dist/client/assets/{qml-BiamCZ_t.js → qml-CtVRO65R.js} +1 -1
- package/dist/client/assets/{r-DR_qiHtN.js → r-BhAilYXn.js} +1 -1
- package/dist/client/assets/{razor-CWQHCbLD.js → razor-r2okdCFg.js} +1 -1
- package/dist/client/assets/{regexp-BX6N0GwV.js → regexp-Du1vBRb3.js} +1 -1
- package/dist/client/assets/{rst-Cphc8U5i.js → rst-Bh3lie48.js} +1 -1
- package/dist/client/assets/{ruby-e-dOujhw.js → ruby-vs3FXmpi.js} +1 -1
- package/dist/client/assets/{sas-CLccRu7Y.js → sas-DkxBsIWw.js} +1 -1
- package/dist/client/assets/{scss-DddDFRbZ.js → scss-dEqn8th9.js} +1 -1
- package/dist/client/assets/{shellscript-BRMFd79r.js → shellscript-DuK80VLO.js} +1 -1
- package/dist/client/assets/{shellsession-mBDRZscv.js → shellsession-K16miZZV.js} +1 -1
- package/dist/client/assets/{soy-CWg8ljNL.js → soy-DseiXgsm.js} +1 -1
- package/dist/client/assets/{sql-Bpvi_qeu.js → sql-aXIOcbM3.js} +1 -1
- package/dist/client/assets/{stata-DmDI8_ZD.js → stata-CEH4kh2r.js} +1 -1
- package/dist/client/assets/{surrealql-BK43wnCn.js → surrealql-COLqIpYI.js} +1 -1
- package/dist/client/assets/{svelte-CqNHamjt.js → svelte-Bv_6pJZf.js} +1 -1
- package/dist/client/assets/{templ-CXX6LdND.js → templ-BK2D_AFL.js} +1 -1
- package/dist/client/assets/{tex-B5fWHM8G.js → tex--R7BXLUz.js} +1 -1
- package/dist/client/assets/{ts-tags-B4vli5O4.js → ts-tags-C_tPUBdU.js} +1 -1
- package/dist/client/assets/{tsx-BuPcRuDt.js → tsx-CkHIsN3A.js} +1 -1
- package/dist/client/assets/{twig-Dde1JkFC.js → twig-Br2AoIs8.js} +1 -1
- package/dist/client/assets/{typescript-BSNd5gmZ.js → typescript-C733jxER.js} +1 -1
- package/dist/client/assets/{typst-6P_s0OU3.js → typst-1qUjIU5z.js} +1 -1
- package/dist/client/assets/{vue-CDLX_0xc.js → vue-DtvZUYyv.js} +1 -1
- package/dist/client/assets/{vue-html-DOu8fRko.js → vue-html-Db1X0s7Z.js} +1 -1
- package/dist/client/assets/{vue-vine-Bi0JP7vZ.js → vue-vine-DQulvS_V.js} +1 -1
- package/dist/client/assets/{xml-BT2xddIz.js → xml-DY9-QC9A.js} +1 -1
- package/dist/client/assets/{xsl-BAcHcoe6.js → xsl-LICYc3rB.js} +1 -1
- package/dist/client/assets/{yaml-Dqw9UXyh.js → yaml-DmBUKFWe.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/main.js +1809 -1122
- package/package.json +7 -1
- package/dist/client/assets/index-DLiMd279.css +0 -2
- package/dist/client/assets/index-WLpwHHb8.js +0 -1787
package/dist/server/main.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { EventEmitter } from "node:events";
|
|
4
4
|
import childProcess, { execFile, spawn } from "node:child_process";
|
|
5
|
-
import path, { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
-
import fs, { accessSync, constants, existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
5
|
+
import path, { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import fs, { accessSync, constants, existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
7
7
|
import process$1 from "node:process";
|
|
8
8
|
import { stripVTControlCharacters } from "node:util";
|
|
9
9
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
@@ -3952,7 +3952,7 @@ function generateCompletion(program, { shell, executable = program.name() }) {
|
|
|
3952
3952
|
}
|
|
3953
3953
|
//#endregion
|
|
3954
3954
|
//#region package.json
|
|
3955
|
-
var version$1 = "0.2.
|
|
3955
|
+
var version$1 = "0.2.7";
|
|
3956
3956
|
//#endregion
|
|
3957
3957
|
//#region src/server/comments/anchor.ts
|
|
3958
3958
|
function splitLines(contents) {
|
|
@@ -11401,7 +11401,7 @@ const ZodType = /*@__PURE__*/ $constructor("ZodType", (inst, def) => {
|
|
|
11401
11401
|
return array(this);
|
|
11402
11402
|
},
|
|
11403
11403
|
or(arg) {
|
|
11404
|
-
return union([this, arg]);
|
|
11404
|
+
return union$1([this, arg]);
|
|
11405
11405
|
},
|
|
11406
11406
|
and(arg) {
|
|
11407
11407
|
return intersection(this, arg);
|
|
@@ -11950,7 +11950,7 @@ const ZodUnion = /*@__PURE__*/ $constructor("ZodUnion", (inst, def) => {
|
|
|
11950
11950
|
inst._zod.processJSONSchema = (ctx, json, params) => unionProcessor(inst, ctx, json, params);
|
|
11951
11951
|
inst.options = def.options;
|
|
11952
11952
|
});
|
|
11953
|
-
function union(options, params) {
|
|
11953
|
+
function union$1(options, params) {
|
|
11954
11954
|
return new ZodUnion({
|
|
11955
11955
|
type: "union",
|
|
11956
11956
|
options,
|
|
@@ -12238,10 +12238,14 @@ function superRefine(fn, params) {
|
|
|
12238
12238
|
* server is told in `didOpen`, so it must be the spec's spelling, not ours.
|
|
12239
12239
|
*/
|
|
12240
12240
|
const LanguageIdSchema = _enum([
|
|
12241
|
+
"astro",
|
|
12241
12242
|
"c",
|
|
12242
12243
|
"cpp",
|
|
12244
|
+
"csharp",
|
|
12245
|
+
"css",
|
|
12243
12246
|
"go",
|
|
12244
12247
|
"haskell",
|
|
12248
|
+
"html",
|
|
12245
12249
|
"java",
|
|
12246
12250
|
"javascript",
|
|
12247
12251
|
"javascriptreact",
|
|
@@ -12260,6 +12264,7 @@ const LanguageIdSchema = _enum([
|
|
|
12260
12264
|
"toml",
|
|
12261
12265
|
"typescript",
|
|
12262
12266
|
"typescriptreact",
|
|
12267
|
+
"vue",
|
|
12263
12268
|
"yaml",
|
|
12264
12269
|
"zig"
|
|
12265
12270
|
]);
|
|
@@ -12681,6 +12686,8 @@ object({
|
|
|
12681
12686
|
contextLines: number(),
|
|
12682
12687
|
/** When a review between refs recomputes as its refs move; see `FollowRefsSchema`. Default auto. */
|
|
12683
12688
|
followRefs: FollowRefsSchema,
|
|
12689
|
+
/** Wrap long lines in the viewer instead of scrolling them sideways. Default true. */
|
|
12690
|
+
lineWrap: boolean(),
|
|
12684
12691
|
/**
|
|
12685
12692
|
* Language server command per language, replacing the built-in candidate for it; an empty
|
|
12686
12693
|
* string disables the language. Read-only over HTTP; set with `diffle config set-lsp`.
|
|
@@ -12691,6 +12698,7 @@ const DEFAULT_USER_CONFIG = {
|
|
|
12691
12698
|
autoViewed: [],
|
|
12692
12699
|
contextLines: 5,
|
|
12693
12700
|
followRefs: "auto",
|
|
12701
|
+
lineWrap: true,
|
|
12694
12702
|
lspCommands: {}
|
|
12695
12703
|
};
|
|
12696
12704
|
/** A point in a new-side file. LSP semantics: the column counts UTF-16 units, like a JS string index. */
|
|
@@ -12710,8 +12718,8 @@ const LspLocationSchema = LspPositionSchema.extend({
|
|
|
12710
12718
|
*/
|
|
12711
12719
|
external: literal(true).optional()
|
|
12712
12720
|
});
|
|
12713
|
-
/**
|
|
12714
|
-
const
|
|
12721
|
+
/** One language server process, as its bridge sees it. */
|
|
12722
|
+
const LspProcessStatusSchema = object({
|
|
12715
12723
|
/** Program name, for messages: the basename of the command's first word, e.g. `pyrefly`. */
|
|
12716
12724
|
name: string(),
|
|
12717
12725
|
/** The command line it was started from. */
|
|
@@ -12725,19 +12733,83 @@ const LspServerStatusSchema = object({
|
|
|
12725
12733
|
message: string().optional(),
|
|
12726
12734
|
/** Active work reported through LSP work-done progress; absent when none is reported. */
|
|
12727
12735
|
activity: string().array().optional(),
|
|
12728
|
-
/**
|
|
12736
|
+
/**
|
|
12737
|
+
* What the server last said to the reader through `window/showMessage`, when it was a
|
|
12738
|
+
* warning or an error. Its log never stands here: that is `log`.
|
|
12739
|
+
*/
|
|
12729
12740
|
notice: object({
|
|
12730
12741
|
severity: _enum(["warning", "error"]),
|
|
12731
12742
|
message: string()
|
|
12732
12743
|
}).optional(),
|
|
12733
|
-
/**
|
|
12734
|
-
|
|
12735
|
-
|
|
12744
|
+
/** The server's output channel in brief; `GET /api/lsp/log` lists the lines. */
|
|
12745
|
+
log: object({
|
|
12746
|
+
/** Changes whenever a line is added or repeated, so a reader showing the log knows to fetch again. */
|
|
12747
|
+
seq: number().int().nonnegative(),
|
|
12748
|
+
/** Error and warning lines since the server started, kept or not. */
|
|
12749
|
+
errors: number().int().nonnegative(),
|
|
12750
|
+
warnings: number().int().nonnegative()
|
|
12751
|
+
})
|
|
12752
|
+
});
|
|
12753
|
+
/** Where a log line came from: `window/showMessage`, `window/logMessage`, or the process's stderr. */
|
|
12754
|
+
const LspLogSourceSchema = _enum([
|
|
12755
|
+
"message",
|
|
12756
|
+
"log",
|
|
12757
|
+
"stderr"
|
|
12758
|
+
]);
|
|
12759
|
+
const LspLogLevelSchema = _enum([
|
|
12760
|
+
"error",
|
|
12761
|
+
"warning",
|
|
12762
|
+
"info",
|
|
12763
|
+
"log"
|
|
12764
|
+
]);
|
|
12765
|
+
/** One line of a server's output channel. The bridge keeps a bounded number of recent ones. */
|
|
12766
|
+
const LspLogEntrySchema = object({
|
|
12767
|
+
/** Stable identity of the line within the run. */
|
|
12768
|
+
id: number().int().positive(),
|
|
12769
|
+
/** Milliseconds since the epoch, of its latest repeat. */
|
|
12770
|
+
time: number(),
|
|
12771
|
+
source: LspLogSourceSchema,
|
|
12772
|
+
level: LspLogLevelSchema,
|
|
12773
|
+
message: string(),
|
|
12774
|
+
/** How many times in a row the server said exactly this. */
|
|
12775
|
+
count: number().int().positive()
|
|
12776
|
+
});
|
|
12777
|
+
/** `GET /api/lsp/log?server=<command>`: the retained lines of one server, oldest first. */
|
|
12778
|
+
const LspLogQuerySchema = object({ server: string().min(1) });
|
|
12779
|
+
object({ entries: LspLogEntrySchema.array() });
|
|
12780
|
+
/**
|
|
12781
|
+
* A request the bridge sends a server only once it advertised the capability, in its
|
|
12782
|
+
* `initialize` result or by registering it later, as an editor does. Document highlights
|
|
12783
|
+
* are advisory: without them the answer is empty, never an error.
|
|
12784
|
+
*/
|
|
12785
|
+
const LspFeatureSchema = _enum([
|
|
12786
|
+
"definition",
|
|
12787
|
+
"typeDefinition",
|
|
12788
|
+
"hover",
|
|
12789
|
+
"references",
|
|
12790
|
+
"documentSymbol",
|
|
12791
|
+
"workspaceSymbol",
|
|
12792
|
+
"documentHighlight"
|
|
12793
|
+
]);
|
|
12794
|
+
/** A process plus the languages the pool routes to it. */
|
|
12795
|
+
const LspServerStatusSchema = LspProcessStatusSchema.extend({
|
|
12796
|
+
/** Languages it owns: asked first, and alone for highlights, token kinds and outlines. */
|
|
12797
|
+
languages: LanguageIdSchema.array(),
|
|
12798
|
+
/** Languages it answers for beside their owner, such as Tailwind's class hovers in markup. */
|
|
12799
|
+
joins: LanguageIdSchema.array(),
|
|
12800
|
+
/** What its `initialize` result advertised; empty while starting. */
|
|
12801
|
+
supports: LspFeatureSchema.array()
|
|
12802
|
+
});
|
|
12736
12803
|
/** A language in the diff with no server behind it. */
|
|
12737
12804
|
const LspMissingSchema = object({
|
|
12738
12805
|
language: LanguageIdSchema,
|
|
12739
12806
|
/** Programs looked for on PATH, so the UI can name what to install. Empty when config turned the language off. */
|
|
12740
|
-
tried: string().array()
|
|
12807
|
+
tried: string().array(),
|
|
12808
|
+
/**
|
|
12809
|
+
* A program that is on PATH but lacks what the server loads from beside it, such as a
|
|
12810
|
+
* `vue-language-server` without its tsserver plugin: installing more is not the answer.
|
|
12811
|
+
*/
|
|
12812
|
+
why: string().optional()
|
|
12741
12813
|
});
|
|
12742
12814
|
const LspStatusSchema = object({
|
|
12743
12815
|
/** False with `--no-lsp`: no server will start for this run. */
|
|
@@ -12746,7 +12818,16 @@ const LspStatusSchema = object({
|
|
|
12746
12818
|
servers: LspServerStatusSchema.array(),
|
|
12747
12819
|
missing: LspMissingSchema.array()
|
|
12748
12820
|
});
|
|
12749
|
-
|
|
12821
|
+
/**
|
|
12822
|
+
* Why an empty answer may be incomplete: the server reported background work (indexing, loading
|
|
12823
|
+
* the workspace) while answering, or a server that would also have answered was unavailable.
|
|
12824
|
+
* Never set beside a non-empty answer.
|
|
12825
|
+
*/
|
|
12826
|
+
const inconclusive$1 = string().optional();
|
|
12827
|
+
object({
|
|
12828
|
+
locations: LspLocationSchema.array(),
|
|
12829
|
+
inconclusive: inconclusive$1
|
|
12830
|
+
});
|
|
12750
12831
|
object({
|
|
12751
12832
|
/** Markdown; null when the server has nothing for the position. */
|
|
12752
12833
|
contents: string().nullable(),
|
|
@@ -12756,11 +12837,9 @@ object({
|
|
|
12756
12837
|
col: number(),
|
|
12757
12838
|
endLine: number(),
|
|
12758
12839
|
endCol: number()
|
|
12759
|
-
}).optional()
|
|
12840
|
+
}).optional(),
|
|
12841
|
+
inconclusive: inconclusive$1
|
|
12760
12842
|
});
|
|
12761
|
-
object({
|
|
12762
|
-
/** Null when the position is not inside a token or the server does not classify tokens. */
|
|
12763
|
-
kind: string().nullable() });
|
|
12764
12843
|
object({ occurrences: object({
|
|
12765
12844
|
line: number().int().positive(),
|
|
12766
12845
|
col: number().int().nonnegative(),
|
|
@@ -12774,16 +12853,19 @@ object({ occurrences: object({
|
|
|
12774
12853
|
])
|
|
12775
12854
|
}).array() });
|
|
12776
12855
|
object({
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
12856
|
+
symbols: object({
|
|
12857
|
+
name: string(),
|
|
12858
|
+
/** LSP SymbolKind number; the client maps it to a label. */
|
|
12859
|
+
kind: number(),
|
|
12860
|
+
/** Enclosing class or function, when any. */
|
|
12861
|
+
container: string().optional(),
|
|
12862
|
+
path: string(),
|
|
12863
|
+
line: number(),
|
|
12864
|
+
/** Last line of the declaration's full range. */
|
|
12865
|
+
endLine: number(),
|
|
12866
|
+
col: number()
|
|
12867
|
+
}).array(),
|
|
12868
|
+
inconclusive: inconclusive$1
|
|
12787
12869
|
});
|
|
12788
12870
|
_enum([
|
|
12789
12871
|
"unviewed",
|
|
@@ -12813,11 +12895,14 @@ function followsCheckout(snap) {
|
|
|
12813
12895
|
}
|
|
12814
12896
|
/** Extension (no dot, lowercase) → language. `.h` goes to c because clangd serves both. */
|
|
12815
12897
|
const LANGUAGE_BY_EXT = {
|
|
12898
|
+
astro: "astro",
|
|
12816
12899
|
bash: "shellscript",
|
|
12817
12900
|
c: "c",
|
|
12818
12901
|
cc: "cpp",
|
|
12819
12902
|
cjs: "javascript",
|
|
12820
12903
|
cpp: "cpp",
|
|
12904
|
+
cs: "csharp",
|
|
12905
|
+
css: "css",
|
|
12821
12906
|
cts: "typescript",
|
|
12822
12907
|
cxx: "cpp",
|
|
12823
12908
|
go: "go",
|
|
@@ -12825,6 +12910,8 @@ const LANGUAGE_BY_EXT = {
|
|
|
12825
12910
|
hh: "cpp",
|
|
12826
12911
|
hpp: "cpp",
|
|
12827
12912
|
hs: "haskell",
|
|
12913
|
+
htm: "html",
|
|
12914
|
+
html: "html",
|
|
12828
12915
|
hxx: "cpp",
|
|
12829
12916
|
java: "java",
|
|
12830
12917
|
js: "javascript",
|
|
@@ -12849,6 +12936,7 @@ const LANGUAGE_BY_EXT = {
|
|
|
12849
12936
|
toml: "toml",
|
|
12850
12937
|
ts: "typescript",
|
|
12851
12938
|
tsx: "typescriptreact",
|
|
12939
|
+
vue: "vue",
|
|
12852
12940
|
yaml: "yaml",
|
|
12853
12941
|
yml: "yaml",
|
|
12854
12942
|
zig: "zig"
|
|
@@ -12860,40 +12948,12 @@ function languageOf(path) {
|
|
|
12860
12948
|
const ext = /\.([^./\\]+)$/.exec(path)?.[1]?.toLowerCase();
|
|
12861
12949
|
return ext && LANGUAGE_BY_EXT[ext] || null;
|
|
12862
12950
|
}
|
|
12863
|
-
/**
|
|
12864
|
-
* Why the language server cannot answer for `path` — or, with no path, for a
|
|
12865
|
-
* repository-wide request — and null when it can. Shared so the client's flash and the
|
|
12866
|
-
* API's 409 say the same thing. The caller checks `followsCheckout` itself.
|
|
12867
|
-
*/
|
|
12868
|
-
function lspBlocker(lsp, path) {
|
|
12869
|
-
if (!lsp.enabled) return "Language servers are off for this run (--no-lsp)";
|
|
12870
|
-
if (path == null) {
|
|
12871
|
-
if (!lsp.servers.length) return noServer(lsp);
|
|
12872
|
-
return lsp.servers.some((s) => s.state === "ready") ? null : notReady(lsp.servers[0]);
|
|
12873
|
-
}
|
|
12874
|
-
const language = languageOf(path);
|
|
12875
|
-
if (!language) return `No language server for ${/(\.[^./\\]+)$/.exec(path)?.[1] ?? "these"} files`;
|
|
12876
|
-
const server = lsp.servers.find((s) => s.languages.includes(language));
|
|
12877
|
-
if (!server) return noServer(lsp, language);
|
|
12878
|
-
return server.state === "ready" ? null : notReady(server);
|
|
12879
|
-
}
|
|
12880
|
-
function notReady(s) {
|
|
12881
|
-
return s.state === "starting" ? `${s.name} is starting…` : `${s.name} is unavailable: ${s.message ?? "unknown error"}`;
|
|
12882
|
-
}
|
|
12883
|
-
/** No server serves `language` (or nothing runs at all): say what the reader can do about it. */
|
|
12884
|
-
function noServer(lsp, language) {
|
|
12885
|
-
const missing = language == null ? lsp.missing : lsp.missing.filter((m) => m.language === language);
|
|
12886
|
-
const absent = missing.filter((m) => m.tried.length > 0);
|
|
12887
|
-
const off = missing.filter((m) => m.tried.length === 0).map((m) => m.language);
|
|
12888
|
-
if (absent.length) return `No language server for ${[...new Set(absent.map((m) => m.language))].join(", ")} on PATH: install one of ${[...new Set(absent.flatMap((m) => m.tried))].join(", ")}`;
|
|
12889
|
-
if (off.length) return `The language server for ${off.join(", ")} is off in your diffle config`;
|
|
12890
|
-
return language == null ? "No language server runs for this diff" : `No language server runs for ${language}`;
|
|
12891
|
-
}
|
|
12892
12951
|
/** Writable HTTP config fields; unknown keys, including CLI-only lspCommands, are stripped. */
|
|
12893
12952
|
const ConfigUpdateSchema = object({
|
|
12894
12953
|
autoViewed: string().array().optional(),
|
|
12895
12954
|
contextLines: number().int().min(0).max(1e4).optional(),
|
|
12896
|
-
followRefs: FollowRefsSchema.optional()
|
|
12955
|
+
followRefs: FollowRefsSchema.optional(),
|
|
12956
|
+
lineWrap: boolean().optional()
|
|
12897
12957
|
});
|
|
12898
12958
|
const ResolvedRequestSchema = object({ resolved: boolean() });
|
|
12899
12959
|
const ViewedBulkRequestSchema = object({ entries: ViewedEntrySchema.array() });
|
|
@@ -13009,46 +13069,158 @@ function fuzzyRank(items, query, key) {
|
|
|
13009
13069
|
return [...matched, ...items.filter((item) => !kept.has(item))];
|
|
13010
13070
|
}
|
|
13011
13071
|
//#endregion
|
|
13012
|
-
//#region src/
|
|
13072
|
+
//#region src/shared/lsp.ts
|
|
13073
|
+
/** How messages name a feature: "pyrefly offers no go to definition". */
|
|
13074
|
+
const LSP_FEATURE_LABELS = {
|
|
13075
|
+
definition: "go to definition",
|
|
13076
|
+
typeDefinition: "go to type definition",
|
|
13077
|
+
hover: "hover",
|
|
13078
|
+
references: "references",
|
|
13079
|
+
documentSymbol: "document symbols",
|
|
13080
|
+
workspaceSymbol: "workspace symbols",
|
|
13081
|
+
documentHighlight: "document highlights"
|
|
13082
|
+
};
|
|
13013
13083
|
/**
|
|
13014
|
-
*
|
|
13015
|
-
*
|
|
13016
|
-
*
|
|
13084
|
+
* Why no language server can answer the query right now, and null when one can. The one
|
|
13085
|
+
* place that reasons about the run, the snapshot and the servers together, so the
|
|
13086
|
+
* client's flash and the API's 409 say the same thing. A null `snap` is one not loaded yet.
|
|
13017
13087
|
*/
|
|
13018
|
-
function
|
|
13019
|
-
if (
|
|
13020
|
-
|
|
13021
|
-
|
|
13088
|
+
function lspBlocker(lsp, snap, { path, feature, patient = false } = {}) {
|
|
13089
|
+
if (!lsp.enabled) return "Language servers are off for this run (--no-lsp)";
|
|
13090
|
+
if (!snap || !followsCheckout(snap)) return "Symbol navigation needs the new side to be the worktree or the checked-out commit";
|
|
13091
|
+
let servers = lsp.servers;
|
|
13092
|
+
let language;
|
|
13093
|
+
if (path != null) {
|
|
13094
|
+
language = languageOf(path) ?? void 0;
|
|
13095
|
+
if (!language) return `No language server for ${/(\.[^./\\]+)$/.exec(path)?.[1] ?? "these"} files`;
|
|
13096
|
+
servers = serversFor(lsp, language);
|
|
13097
|
+
}
|
|
13098
|
+
if (!servers.length) return noServer(lsp, language);
|
|
13099
|
+
const able = feature ? servers.filter((s) => s.state === "starting" || s.supports.includes(feature)) : servers;
|
|
13100
|
+
if (!able.length) return `${servers.map((s) => s.name).join(", ")} offers no ${LSP_FEATURE_LABELS[feature]}`;
|
|
13101
|
+
return able.some((s) => s.state === "ready" || patient && s.state === "starting") ? null : notReady(able[0]);
|
|
13102
|
+
}
|
|
13103
|
+
/** The servers answering for `language`, its owners before the ones joining it. */
|
|
13104
|
+
function serversFor(lsp, language) {
|
|
13105
|
+
return [...lsp.servers.filter((s) => s.languages.includes(language)), ...lsp.servers.filter((s) => s.joins.includes(language))];
|
|
13106
|
+
}
|
|
13107
|
+
/** A server's languages for a status line: the ones it owns, then the ones it joins marked `+`. */
|
|
13108
|
+
function languagesLabel(server) {
|
|
13109
|
+
return [...server.languages, ...server.joins.map((l) => `+${l}`)].join(", ");
|
|
13022
13110
|
}
|
|
13023
|
-
function
|
|
13024
|
-
|
|
13025
|
-
if ("kind" in part) return part.kind === "markdown" ? part.value : fence(part.value, "");
|
|
13026
|
-
return fence(part.value, part.language);
|
|
13111
|
+
function notReady(s) {
|
|
13112
|
+
return s.state === "starting" ? `${s.name} is starting…` : `${s.name} is unavailable: ${s.message ?? "unknown error"}`;
|
|
13027
13113
|
}
|
|
13028
|
-
/**
|
|
13029
|
-
function
|
|
13030
|
-
const
|
|
13031
|
-
const
|
|
13032
|
-
return
|
|
13114
|
+
/** No server serves `language` (or nothing runs at all): say what the reader can do about it. */
|
|
13115
|
+
function noServer(lsp, language) {
|
|
13116
|
+
const missing = language == null ? lsp.missing : lsp.missing.filter((m) => m.language === language);
|
|
13117
|
+
const broken = missing.find((m) => m.why);
|
|
13118
|
+
if (broken) return `No language server for ${broken.language}: ${broken.why}`;
|
|
13119
|
+
const absent = missing.filter((m) => m.tried.length > 0);
|
|
13120
|
+
const off = missing.filter((m) => m.tried.length === 0).map((m) => m.language);
|
|
13121
|
+
if (absent.length) return `No language server for ${[...new Set(absent.map((m) => m.language))].join(", ")} on PATH: install one of ${[...new Set(absent.flatMap((m) => m.tried))].join(", ")}`;
|
|
13122
|
+
if (off.length) return `The language server for ${off.join(", ")} is off in your diffle config`;
|
|
13123
|
+
return language == null ? "No language server runs for this diff" : `No language server runs for ${language}`;
|
|
13033
13124
|
}
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
/**
|
|
13037
|
-
|
|
13038
|
-
|
|
13125
|
+
//#endregion
|
|
13126
|
+
//#region src/server/persist.ts
|
|
13127
|
+
/** A holder older than this is presumed hung, even when its pid is alive. */
|
|
13128
|
+
const LOCK_STALE_MS = 3e4;
|
|
13129
|
+
const LOCK_TIMEOUT_MS = 1e4;
|
|
13130
|
+
let seq = 0;
|
|
13131
|
+
/**
|
|
13132
|
+
* Writes `text` to `file` atomically: unique temp file in the same directory,
|
|
13133
|
+
* then rename. Creates the directory owner-only; the file gets `mode`. A failed
|
|
13134
|
+
* write leaves no temp file behind.
|
|
13135
|
+
*/
|
|
13136
|
+
async function writeFileAtomic(file, text, mode = 384) {
|
|
13137
|
+
await mkdir(dirname(file), {
|
|
13138
|
+
recursive: true,
|
|
13139
|
+
mode: 448
|
|
13140
|
+
});
|
|
13141
|
+
const tmp = `${file}.${process.pid}.${++seq}.${randomBytes(4).toString("hex")}.tmp`;
|
|
13142
|
+
try {
|
|
13143
|
+
await writeFile(tmp, text, {
|
|
13144
|
+
encoding: "utf8",
|
|
13145
|
+
mode
|
|
13146
|
+
});
|
|
13147
|
+
await rename(tmp, file);
|
|
13148
|
+
} catch (e) {
|
|
13149
|
+
await unlink(tmp).catch(() => {});
|
|
13150
|
+
throw e;
|
|
13151
|
+
}
|
|
13039
13152
|
}
|
|
13153
|
+
const inProcess = /* @__PURE__ */ new Map();
|
|
13040
13154
|
/**
|
|
13041
|
-
*
|
|
13042
|
-
*
|
|
13043
|
-
*
|
|
13044
|
-
*
|
|
13155
|
+
* Runs `fn` while holding `<file>.lock`, exclusive across processes on this
|
|
13156
|
+
* machine and serialized within this one. A lock whose pid is dead, or older
|
|
13157
|
+
* than 30s, is taken over; waiting longer than 10s throws. The lock is
|
|
13158
|
+
* released whether `fn` resolves or rejects.
|
|
13045
13159
|
*/
|
|
13046
|
-
function
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13160
|
+
async function withFileLock(file, fn) {
|
|
13161
|
+
const prev = inProcess.get(file) ?? Promise.resolve();
|
|
13162
|
+
let release;
|
|
13163
|
+
const chain = prev.then(() => new Promise((r) => release = r));
|
|
13164
|
+
inProcess.set(file, chain);
|
|
13165
|
+
await prev;
|
|
13166
|
+
const lock = `${file}.lock`;
|
|
13167
|
+
try {
|
|
13168
|
+
await acquire(lock);
|
|
13169
|
+
try {
|
|
13170
|
+
return await fn();
|
|
13171
|
+
} finally {
|
|
13172
|
+
await unlink(lock).catch(() => {});
|
|
13173
|
+
}
|
|
13174
|
+
} finally {
|
|
13175
|
+
release();
|
|
13176
|
+
if (inProcess.get(file) === chain) inProcess.delete(file);
|
|
13177
|
+
}
|
|
13178
|
+
}
|
|
13179
|
+
async function acquire(lock) {
|
|
13180
|
+
await mkdir(dirname(lock), {
|
|
13181
|
+
recursive: true,
|
|
13182
|
+
mode: 448
|
|
13051
13183
|
});
|
|
13184
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
13185
|
+
for (;;) {
|
|
13186
|
+
try {
|
|
13187
|
+
const fh = await open(lock, "wx", 384);
|
|
13188
|
+
try {
|
|
13189
|
+
await fh.writeFile(`${process.pid}\n`);
|
|
13190
|
+
} finally {
|
|
13191
|
+
await fh.close();
|
|
13192
|
+
}
|
|
13193
|
+
return;
|
|
13194
|
+
} catch (e) {
|
|
13195
|
+
if (e.code !== "EEXIST") throw e;
|
|
13196
|
+
}
|
|
13197
|
+
if (await isStale(lock)) {
|
|
13198
|
+
await unlink(lock).catch(() => {});
|
|
13199
|
+
continue;
|
|
13200
|
+
}
|
|
13201
|
+
if (Date.now() > deadline) throw new Error(`timed out waiting for ${lock}`);
|
|
13202
|
+
await new Promise((r) => setTimeout(r, 10 + Math.random() * 20));
|
|
13203
|
+
}
|
|
13204
|
+
}
|
|
13205
|
+
/** True when the holder is gone: dead pid, or too old to trust. A lock with no pid yet is being taken. */
|
|
13206
|
+
async function isStale(lock) {
|
|
13207
|
+
let st;
|
|
13208
|
+
let text;
|
|
13209
|
+
try {
|
|
13210
|
+
[st, text] = await Promise.all([stat$1(lock), readFile(lock, "utf8")]);
|
|
13211
|
+
} catch (e) {
|
|
13212
|
+
if (e.code === "ENOENT") return false;
|
|
13213
|
+
throw e;
|
|
13214
|
+
}
|
|
13215
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) return true;
|
|
13216
|
+
const pid = Number.parseInt(text, 10);
|
|
13217
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
13218
|
+
try {
|
|
13219
|
+
process.kill(pid, 0);
|
|
13220
|
+
return false;
|
|
13221
|
+
} catch (e) {
|
|
13222
|
+
return e.code === "ESRCH";
|
|
13223
|
+
}
|
|
13052
13224
|
}
|
|
13053
13225
|
//#endregion
|
|
13054
13226
|
//#region src/server/lsp/JsonRpc.ts
|
|
@@ -13106,8 +13278,8 @@ var JsonRpcConnection = class {
|
|
|
13106
13278
|
params
|
|
13107
13279
|
});
|
|
13108
13280
|
}
|
|
13109
|
-
/** Observes server
|
|
13110
|
-
|
|
13281
|
+
/** Observes every message the server sends: notifications and requests alike. */
|
|
13282
|
+
onMessage(handler) {
|
|
13111
13283
|
this.handlers.add(handler);
|
|
13112
13284
|
}
|
|
13113
13285
|
/** Handles a server request and returns its result, including asynchronous results. */
|
|
@@ -13146,10 +13318,10 @@ var JsonRpcConnection = class {
|
|
|
13146
13318
|
if (this.buffer.length < start + length) return;
|
|
13147
13319
|
const body = this.buffer.subarray(start, start + length).toString("utf8");
|
|
13148
13320
|
this.buffer = this.buffer.subarray(start + length);
|
|
13149
|
-
this.
|
|
13321
|
+
this.receive(body);
|
|
13150
13322
|
}
|
|
13151
13323
|
}
|
|
13152
|
-
|
|
13324
|
+
receive(body) {
|
|
13153
13325
|
let msg;
|
|
13154
13326
|
try {
|
|
13155
13327
|
msg = JSON.parse(body);
|
|
@@ -13186,552 +13358,497 @@ var JsonRpcConnection = class {
|
|
|
13186
13358
|
}
|
|
13187
13359
|
};
|
|
13188
13360
|
//#endregion
|
|
13189
|
-
//#region src/server/lsp/
|
|
13190
|
-
|
|
13191
|
-
|
|
13192
|
-
|
|
13193
|
-
|
|
13194
|
-
|
|
13195
|
-
|
|
13196
|
-
|
|
13197
|
-
|
|
13198
|
-
|
|
13199
|
-
|
|
13200
|
-
|
|
13201
|
-
|
|
13202
|
-
|
|
13203
|
-
|
|
13361
|
+
//#region src/server/lsp/configuration.ts
|
|
13362
|
+
const SCHEMA_CATALOG = "https://www.schemastore.org/api/json/catalog.json";
|
|
13363
|
+
const MAX_AGE = 864e5;
|
|
13364
|
+
const AssociationSchema = object({
|
|
13365
|
+
url: url({ protocol: /^https?$/ }),
|
|
13366
|
+
fileMatch: array(string()).nonempty()
|
|
13367
|
+
});
|
|
13368
|
+
const SchemaCatalogSchema = object({ schemas: array(unknown()) });
|
|
13369
|
+
const ConfigurationParamsSchema = object({ items: array(object({ section: string().optional() })) });
|
|
13370
|
+
/** Valid catalog entries, in the association format understood by the JSON server. */
|
|
13371
|
+
function schemaAssociations(value) {
|
|
13372
|
+
const catalog = SchemaCatalogSchema.safeParse(value);
|
|
13373
|
+
if (!catalog.success) return [];
|
|
13374
|
+
return catalog.data.schemas.flatMap((entry) => {
|
|
13375
|
+
const association = AssociationSchema.safeParse(entry);
|
|
13376
|
+
return association.success ? [association.data] : [];
|
|
13377
|
+
});
|
|
13204
13378
|
}
|
|
13205
|
-
/**
|
|
13206
|
-
|
|
13207
|
-
|
|
13208
|
-
|
|
13209
|
-
|
|
13210
|
-
|
|
13211
|
-
|
|
13212
|
-
|
|
13213
|
-
|
|
13214
|
-
if (!
|
|
13215
|
-
const
|
|
13216
|
-
if (
|
|
13379
|
+
/** Caches catalog discovery for a day and uses the last good copy when offline. */
|
|
13380
|
+
async function loadSchemaCatalog(cacheFile = join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "diffle", "schemastore.json"), request = fetch) {
|
|
13381
|
+
let cached = [];
|
|
13382
|
+
try {
|
|
13383
|
+
cached = schemaAssociations(JSON.parse(await readFile(cacheFile, "utf8")));
|
|
13384
|
+
if (cached.length && Date.now() - (await stat$1(cacheFile)).mtimeMs < MAX_AGE) return cached;
|
|
13385
|
+
} catch {}
|
|
13386
|
+
try {
|
|
13387
|
+
const response = await request(SCHEMA_CATALOG, { signal: AbortSignal.timeout(5e3) });
|
|
13388
|
+
if (!response.ok) return cached;
|
|
13389
|
+
const schemas = schemaAssociations(await response.json());
|
|
13390
|
+
if (!schemas.length) return cached;
|
|
13391
|
+
await writeFileAtomic(cacheFile, JSON.stringify({ schemas }) + "\n").catch(() => {});
|
|
13392
|
+
return schemas;
|
|
13393
|
+
} catch {
|
|
13394
|
+
return cached;
|
|
13217
13395
|
}
|
|
13218
|
-
return null;
|
|
13219
13396
|
}
|
|
13220
13397
|
/**
|
|
13221
|
-
*
|
|
13222
|
-
*
|
|
13398
|
+
* Answers a language server's `workspace/configuration` request from `settings`:
|
|
13399
|
+
* each requested section in order, null for unknown sections, and the whole tree
|
|
13400
|
+
* for an omitted section. Params are untrusted JSON from the server; malformed
|
|
13401
|
+
* requests throw JSON-RPC Invalid Params before any settings are returned.
|
|
13223
13402
|
*/
|
|
13224
|
-
function
|
|
13225
|
-
|
|
13226
|
-
if (
|
|
13227
|
-
|
|
13228
|
-
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
|
|
13236
|
-
} catch {
|
|
13237
|
-
return false;
|
|
13238
|
-
}
|
|
13403
|
+
function configurationItems(settings, params) {
|
|
13404
|
+
const request = ConfigurationParamsSchema.safeParse(params);
|
|
13405
|
+
if (!request.success) throw new JsonRpcError(-32602, "Invalid workspace/configuration parameters");
|
|
13406
|
+
return request.data.items.map((item) => {
|
|
13407
|
+
if (!item.section) return settings;
|
|
13408
|
+
let value = settings;
|
|
13409
|
+
for (const part of item.section.split(".")) {
|
|
13410
|
+
if (!value || typeof value !== "object" || !Object.hasOwn(value, part)) return null;
|
|
13411
|
+
value = value[part];
|
|
13412
|
+
}
|
|
13413
|
+
return value ?? null;
|
|
13414
|
+
});
|
|
13239
13415
|
}
|
|
13240
13416
|
//#endregion
|
|
13241
|
-
//#region src/server/
|
|
13242
|
-
/**
|
|
13243
|
-
const
|
|
13244
|
-
|
|
13245
|
-
|
|
13417
|
+
//#region src/server/lsp/ServerOutput.ts
|
|
13418
|
+
/** Log lines kept per server; older ones are dropped. */
|
|
13419
|
+
const LOG_CAPACITY = 200;
|
|
13420
|
+
/** LSP `MessageType` numbers; anything else, such as the newer Debug, reads as a plain log line. */
|
|
13421
|
+
const MESSAGE_LEVELS = {
|
|
13422
|
+
1: "error",
|
|
13423
|
+
2: "warning",
|
|
13424
|
+
3: "info",
|
|
13425
|
+
4: "log"
|
|
13426
|
+
};
|
|
13246
13427
|
/**
|
|
13247
|
-
*
|
|
13248
|
-
*
|
|
13249
|
-
*
|
|
13428
|
+
* What one server reports while it runs: its output channel (what it showed, what it
|
|
13429
|
+
* logged, what it wrote to stderr), the notice it shows the reader, and the progress it
|
|
13430
|
+
* runs. Each change goes to `onChange` as the status fields it moves. Only the log's
|
|
13431
|
+
* summary travels with the status: the lines are fetched by whoever shows them.
|
|
13250
13432
|
*/
|
|
13251
|
-
|
|
13252
|
-
|
|
13253
|
-
|
|
13254
|
-
|
|
13255
|
-
|
|
13256
|
-
|
|
13257
|
-
|
|
13258
|
-
|
|
13259
|
-
|
|
13260
|
-
|
|
13433
|
+
var ServerOutput = class {
|
|
13434
|
+
onChange;
|
|
13435
|
+
/** Oldest first. */
|
|
13436
|
+
log = [];
|
|
13437
|
+
nextLogId = 1;
|
|
13438
|
+
logSummary = {
|
|
13439
|
+
seq: 0,
|
|
13440
|
+
errors: 0,
|
|
13441
|
+
warnings: 0
|
|
13442
|
+
};
|
|
13443
|
+
/** stderr bytes since the last newline. */
|
|
13444
|
+
stderrTail = "";
|
|
13445
|
+
notice;
|
|
13446
|
+
progress = /* @__PURE__ */ new Map();
|
|
13447
|
+
constructor(onChange) {
|
|
13448
|
+
this.onChange = onChange;
|
|
13449
|
+
}
|
|
13450
|
+
/** The retained log lines, oldest first. */
|
|
13451
|
+
entries() {
|
|
13452
|
+
return this.log.map((e) => ({ ...e }));
|
|
13453
|
+
}
|
|
13454
|
+
summary() {
|
|
13455
|
+
return this.logSummary;
|
|
13456
|
+
}
|
|
13457
|
+
/** Whole stderr lines go to the log; a trailing partial line waits for its newline. */
|
|
13458
|
+
stderr(chunk) {
|
|
13459
|
+
const lines = (this.stderrTail + chunk).split("\n");
|
|
13460
|
+
this.stderrTail = lines.pop() ?? "";
|
|
13461
|
+
for (const line of lines) {
|
|
13462
|
+
if (!line.trim()) continue;
|
|
13463
|
+
this.record("stderr", "log", line);
|
|
13464
|
+
}
|
|
13465
|
+
}
|
|
13466
|
+
/** A server-to-client message: `window/*Message*` lands in the log, `$/progress` in the activity. */
|
|
13467
|
+
message(method, params) {
|
|
13468
|
+
if (!params || typeof params !== "object") return;
|
|
13469
|
+
const p = params;
|
|
13470
|
+
if (method === "window/showMessage" || method === "window/logMessage" || method === "window/showMessageRequest") {
|
|
13471
|
+
if (typeof p.message !== "string" || !p.message.trim()) return;
|
|
13472
|
+
const level = MESSAGE_LEVELS[p.type] ?? "log";
|
|
13473
|
+
this.record(method === "window/logMessage" ? "log" : "message", level, p.message);
|
|
13474
|
+
if (method === "window/logMessage" || level !== "error" && level !== "warning") return;
|
|
13475
|
+
const notice = {
|
|
13476
|
+
severity: level,
|
|
13477
|
+
message: p.message.trim()
|
|
13478
|
+
};
|
|
13479
|
+
if (notice.severity === this.notice?.severity && notice.message === this.notice.message) return;
|
|
13480
|
+
this.notice = notice;
|
|
13481
|
+
this.onChange({ notice });
|
|
13482
|
+
return;
|
|
13483
|
+
}
|
|
13484
|
+
if (method === "$/progress") this.noteProgress(p);
|
|
13485
|
+
}
|
|
13486
|
+
/**
|
|
13487
|
+
* Appends to the log, folding a line the server repeats into the previous entry's
|
|
13488
|
+
* count, and reports that the log moved.
|
|
13489
|
+
*/
|
|
13490
|
+
record(source, level, message) {
|
|
13491
|
+
const text = message.trim();
|
|
13492
|
+
if (!text) return;
|
|
13493
|
+
const last = this.log[this.log.length - 1];
|
|
13494
|
+
if (last && last.source === source && last.level === level && last.message === text) {
|
|
13495
|
+
last.count += 1;
|
|
13496
|
+
last.time = Date.now();
|
|
13497
|
+
} else {
|
|
13498
|
+
this.log.push({
|
|
13499
|
+
id: this.nextLogId++,
|
|
13500
|
+
time: Date.now(),
|
|
13501
|
+
source,
|
|
13502
|
+
level,
|
|
13503
|
+
message: text,
|
|
13504
|
+
count: 1
|
|
13505
|
+
});
|
|
13506
|
+
if (this.log.length > LOG_CAPACITY) this.log.shift();
|
|
13507
|
+
}
|
|
13508
|
+
this.logSummary = {
|
|
13509
|
+
seq: this.logSummary.seq + 1,
|
|
13510
|
+
errors: this.logSummary.errors + (level === "error" ? 1 : 0),
|
|
13511
|
+
warnings: this.logSummary.warnings + (level === "warning" ? 1 : 0)
|
|
13512
|
+
};
|
|
13513
|
+
this.onChange({ log: this.logSummary });
|
|
13514
|
+
}
|
|
13515
|
+
/** Work-done progress by token; the activity lists every piece still running. */
|
|
13516
|
+
noteProgress(p) {
|
|
13517
|
+
if (typeof p.token !== "string" && typeof p.token !== "number") return;
|
|
13518
|
+
if (!p.value || typeof p.value !== "object") return;
|
|
13519
|
+
const { kind, title, message, percentage } = p.value;
|
|
13520
|
+
const percent = typeof percentage === "number" && percentage >= 0 && percentage <= 100 ? percentage : void 0;
|
|
13521
|
+
if (kind === "begin" && typeof title === "string") this.progress.set(p.token, {
|
|
13522
|
+
title,
|
|
13523
|
+
message: typeof message === "string" ? message : void 0,
|
|
13524
|
+
percentage: percent
|
|
13261
13525
|
});
|
|
13262
|
-
|
|
13263
|
-
|
|
13264
|
-
|
|
13265
|
-
|
|
13526
|
+
else if (kind === "report" && this.progress.has(p.token)) {
|
|
13527
|
+
if (typeof message === "string") this.progress.get(p.token).message = message;
|
|
13528
|
+
if (percent !== void 0) this.progress.get(p.token).percentage = percent;
|
|
13529
|
+
} else if (kind === "end") {
|
|
13530
|
+
if (!this.progress.delete(p.token)) return;
|
|
13531
|
+
} else return;
|
|
13532
|
+
this.onChange({ activity: [...this.progress.values()].map((p) => `${p.title}${p.percentage == null ? "" : ` (${p.percentage}%)`}${p.message ? `: ${p.message}` : ""}`) });
|
|
13266
13533
|
}
|
|
13534
|
+
};
|
|
13535
|
+
//#endregion
|
|
13536
|
+
//#region src/server/lsp/shapes.ts
|
|
13537
|
+
/**
|
|
13538
|
+
* How a server advertises each feature: the `initialize` capability (a boolean or an
|
|
13539
|
+
* options object) and the method a later `client/registerCapability` names.
|
|
13540
|
+
*/
|
|
13541
|
+
const FEATURES = {
|
|
13542
|
+
definition: {
|
|
13543
|
+
provider: "definitionProvider",
|
|
13544
|
+
method: "textDocument/definition"
|
|
13545
|
+
},
|
|
13546
|
+
typeDefinition: {
|
|
13547
|
+
provider: "typeDefinitionProvider",
|
|
13548
|
+
method: "textDocument/typeDefinition"
|
|
13549
|
+
},
|
|
13550
|
+
hover: {
|
|
13551
|
+
provider: "hoverProvider",
|
|
13552
|
+
method: "textDocument/hover"
|
|
13553
|
+
},
|
|
13554
|
+
references: {
|
|
13555
|
+
provider: "referencesProvider",
|
|
13556
|
+
method: "textDocument/references"
|
|
13557
|
+
},
|
|
13558
|
+
documentSymbol: {
|
|
13559
|
+
provider: "documentSymbolProvider",
|
|
13560
|
+
method: "textDocument/documentSymbol"
|
|
13561
|
+
},
|
|
13562
|
+
workspaceSymbol: {
|
|
13563
|
+
provider: "workspaceSymbolProvider",
|
|
13564
|
+
method: "workspace/symbol"
|
|
13565
|
+
},
|
|
13566
|
+
documentHighlight: {
|
|
13567
|
+
provider: "documentHighlightProvider",
|
|
13568
|
+
method: "textDocument/documentHighlight"
|
|
13569
|
+
}
|
|
13570
|
+
};
|
|
13571
|
+
/** The feature a registration's method names, or null for one diffle never asks for. */
|
|
13572
|
+
function featureOf(method) {
|
|
13573
|
+
for (const [feature, { method: m }] of Object.entries(FEATURES)) if (m === method) return feature;
|
|
13574
|
+
return null;
|
|
13267
13575
|
}
|
|
13268
|
-
const inProcess = /* @__PURE__ */ new Map();
|
|
13269
13576
|
/**
|
|
13270
|
-
*
|
|
13271
|
-
*
|
|
13272
|
-
* than 30s, is taken over; waiting longer than 10s throws. The lock is
|
|
13273
|
-
* released whether `fn` resolves or rejects.
|
|
13577
|
+
* What diffle tells a server it can do. Every feature allows dynamic registration, so a
|
|
13578
|
+
* server that advertises after `initialize` is still asked.
|
|
13274
13579
|
*/
|
|
13275
|
-
|
|
13276
|
-
|
|
13277
|
-
|
|
13278
|
-
|
|
13279
|
-
|
|
13280
|
-
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
|
|
13285
|
-
|
|
13286
|
-
}
|
|
13287
|
-
|
|
13580
|
+
const CLIENT_CAPABILITIES = {
|
|
13581
|
+
window: { workDoneProgress: true },
|
|
13582
|
+
textDocument: {
|
|
13583
|
+
synchronization: { didSave: false },
|
|
13584
|
+
definition: {
|
|
13585
|
+
dynamicRegistration: true,
|
|
13586
|
+
linkSupport: true
|
|
13587
|
+
},
|
|
13588
|
+
typeDefinition: {
|
|
13589
|
+
dynamicRegistration: true,
|
|
13590
|
+
linkSupport: true
|
|
13591
|
+
},
|
|
13592
|
+
references: { dynamicRegistration: true },
|
|
13593
|
+
documentHighlight: { dynamicRegistration: true },
|
|
13594
|
+
hover: {
|
|
13595
|
+
dynamicRegistration: true,
|
|
13596
|
+
contentFormat: ["markdown", "plaintext"]
|
|
13597
|
+
},
|
|
13598
|
+
documentSymbol: {
|
|
13599
|
+
dynamicRegistration: true,
|
|
13600
|
+
hierarchicalDocumentSymbolSupport: true
|
|
13288
13601
|
}
|
|
13289
|
-
}
|
|
13290
|
-
|
|
13291
|
-
|
|
13602
|
+
},
|
|
13603
|
+
workspace: {
|
|
13604
|
+
symbol: { dynamicRegistration: true },
|
|
13605
|
+
workspaceFolders: true,
|
|
13606
|
+
configuration: true
|
|
13292
13607
|
}
|
|
13608
|
+
};
|
|
13609
|
+
/** `answer` as is, or with why it may be incomplete when it is `empty` and the server was `busy`. */
|
|
13610
|
+
function inconclusive(answer, empty, busy) {
|
|
13611
|
+
return busy && empty ? {
|
|
13612
|
+
...answer,
|
|
13613
|
+
inconclusive: busy
|
|
13614
|
+
} : answer;
|
|
13293
13615
|
}
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
}
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
|
|
13302
|
-
|
|
13303
|
-
|
|
13304
|
-
|
|
13305
|
-
|
|
13306
|
-
|
|
13616
|
+
function toLsp(pos) {
|
|
13617
|
+
return {
|
|
13618
|
+
line: Math.max(0, pos.line - 1),
|
|
13619
|
+
character: Math.max(0, pos.col)
|
|
13620
|
+
};
|
|
13621
|
+
}
|
|
13622
|
+
/** An LSP range as diffle spans it: 1-based lines, 0-based columns. */
|
|
13623
|
+
function fromLsp({ start, end }) {
|
|
13624
|
+
return {
|
|
13625
|
+
line: start.line + 1,
|
|
13626
|
+
col: start.character,
|
|
13627
|
+
endLine: end.line + 1,
|
|
13628
|
+
endCol: end.character
|
|
13629
|
+
};
|
|
13630
|
+
}
|
|
13631
|
+
function normalizeLocations(r) {
|
|
13632
|
+
if (!r) return [];
|
|
13633
|
+
return (Array.isArray(r) ? r : [r]).map((x) => "targetUri" in x ? {
|
|
13634
|
+
uri: x.targetUri,
|
|
13635
|
+
range: x.targetSelectionRange ?? x.targetRange
|
|
13636
|
+
} : x);
|
|
13637
|
+
}
|
|
13638
|
+
/** Either symbol shape a server may answer `documentSymbol` with, flattened in document order. */
|
|
13639
|
+
function flattenSymbols(result, path) {
|
|
13640
|
+
const out = [];
|
|
13641
|
+
if (isDocumentSymbols(result)) {
|
|
13642
|
+
const walk = (syms, container) => {
|
|
13643
|
+
for (const s of syms) {
|
|
13644
|
+
const start = (s.selectionRange ?? s.range).start;
|
|
13645
|
+
const endLine = Math.max(start.line, (s.range ?? s.selectionRange).end.line) + 1;
|
|
13646
|
+
out.push({
|
|
13647
|
+
name: s.name,
|
|
13648
|
+
kind: s.kind,
|
|
13649
|
+
container,
|
|
13650
|
+
path,
|
|
13651
|
+
line: start.line + 1,
|
|
13652
|
+
endLine,
|
|
13653
|
+
col: start.character
|
|
13654
|
+
});
|
|
13655
|
+
if (s.children?.length) walk(s.children, s.name);
|
|
13307
13656
|
}
|
|
13308
|
-
|
|
13309
|
-
|
|
13310
|
-
|
|
13311
|
-
}
|
|
13312
|
-
if (await isStale(lock)) {
|
|
13313
|
-
await unlink(lock).catch(() => {});
|
|
13314
|
-
continue;
|
|
13315
|
-
}
|
|
13316
|
-
if (Date.now() > deadline) throw new Error(`timed out waiting for ${lock}`);
|
|
13317
|
-
await new Promise((r) => setTimeout(r, 10 + Math.random() * 20));
|
|
13657
|
+
};
|
|
13658
|
+
walk(result);
|
|
13659
|
+
return out;
|
|
13318
13660
|
}
|
|
13661
|
+
return result.map((s) => symbolInformation(s, path));
|
|
13319
13662
|
}
|
|
13320
|
-
|
|
13321
|
-
|
|
13322
|
-
|
|
13323
|
-
|
|
13324
|
-
|
|
13325
|
-
|
|
13326
|
-
|
|
13327
|
-
|
|
13328
|
-
|
|
13663
|
+
function symbolInformation(s, path) {
|
|
13664
|
+
const start = "range" in s.location ? s.location.range.start : {
|
|
13665
|
+
line: 0,
|
|
13666
|
+
character: 0
|
|
13667
|
+
};
|
|
13668
|
+
const end = "range" in s.location ? s.location.range.end : start;
|
|
13669
|
+
return {
|
|
13670
|
+
name: s.name,
|
|
13671
|
+
kind: s.kind,
|
|
13672
|
+
container: s.containerName || void 0,
|
|
13673
|
+
path,
|
|
13674
|
+
line: start.line + 1,
|
|
13675
|
+
endLine: Math.max(start.line, end.line) + 1,
|
|
13676
|
+
col: start.character
|
|
13677
|
+
};
|
|
13678
|
+
}
|
|
13679
|
+
function isDocumentSymbols(r) {
|
|
13680
|
+
return r.length > 0 && !("location" in r[0]);
|
|
13681
|
+
}
|
|
13682
|
+
//#endregion
|
|
13683
|
+
//#region src/server/lsp/which.ts
|
|
13684
|
+
/**
|
|
13685
|
+
* The program a shell command line runs: its first word, unquoted. Only ever asked
|
|
13686
|
+
* about the built-in candidates, which are plain `prog arg…` lines; a command with a
|
|
13687
|
+
* leading assignment or a pipeline has no single program and is never probed (see
|
|
13688
|
+
* `resolveServers`).
|
|
13689
|
+
*/
|
|
13690
|
+
function argv0(command) {
|
|
13691
|
+
const s = command.trim();
|
|
13692
|
+
const quote = s[0];
|
|
13693
|
+
if (quote === "\"" || quote === "'") {
|
|
13694
|
+
const end = s.indexOf(quote, 1);
|
|
13695
|
+
return end === -1 ? s.slice(1) : s.slice(1, end);
|
|
13329
13696
|
}
|
|
13330
|
-
|
|
13331
|
-
|
|
13332
|
-
|
|
13333
|
-
|
|
13334
|
-
|
|
13335
|
-
|
|
13336
|
-
|
|
13337
|
-
|
|
13697
|
+
return s.split(/\s/, 1)[0] ?? "";
|
|
13698
|
+
}
|
|
13699
|
+
/**
|
|
13700
|
+
* The program a command line runs, as status lines and the registry name it: the basename
|
|
13701
|
+
* of its first word without a Windows extension. `"C:\\tools\\taplo.exe" lsp stdio` → `taplo`.
|
|
13702
|
+
*/
|
|
13703
|
+
function programOf(command) {
|
|
13704
|
+
const prog = argv0(command);
|
|
13705
|
+
return (prog.split(/[\\/]/).pop() || prog).replace(/\.(exe|cmd|bat)$/i, "");
|
|
13706
|
+
}
|
|
13707
|
+
/**
|
|
13708
|
+
* Where `command`'s program lives, or null when it is not an executable on PATH.
|
|
13709
|
+
* A name with a separator is resolved against `cwd` instead of searched, like a shell.
|
|
13710
|
+
*/
|
|
13711
|
+
function findOnPath(command, env = process.env, cwd = process.cwd()) {
|
|
13712
|
+
const prog = argv0(command);
|
|
13713
|
+
if (!prog) return null;
|
|
13714
|
+
if (prog.includes("/") || process.platform === "win32" && prog.includes("\\")) return executable(isAbsolute(prog) ? prog : resolve(cwd, prog));
|
|
13715
|
+
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
13716
|
+
if (!dir) continue;
|
|
13717
|
+
const hit = executable(join(dir, prog));
|
|
13718
|
+
if (hit) return hit;
|
|
13338
13719
|
}
|
|
13720
|
+
return null;
|
|
13339
13721
|
}
|
|
13340
|
-
|
|
13341
|
-
|
|
13342
|
-
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
const ConfigurationParamsSchema = object({ items: array(object({ section: string().optional() })) });
|
|
13350
|
-
/** Valid catalog entries, in the association format understood by the JSON server. */
|
|
13351
|
-
function schemaAssociations(value) {
|
|
13352
|
-
const catalog = SchemaCatalogSchema.safeParse(value);
|
|
13353
|
-
if (!catalog.success) return [];
|
|
13354
|
-
return catalog.data.schemas.flatMap((entry) => {
|
|
13355
|
-
const association = AssociationSchema.safeParse(entry);
|
|
13356
|
-
return association.success ? [association.data] : [];
|
|
13357
|
-
});
|
|
13722
|
+
/**
|
|
13723
|
+
* `file` itself when it is a runnable file, else the first `file` + PATHEXT match on
|
|
13724
|
+
* Windows, where the execute bit means nothing and programs are `.exe`/`.cmd`.
|
|
13725
|
+
*/
|
|
13726
|
+
function executable(file) {
|
|
13727
|
+
if (runnable(file)) return file;
|
|
13728
|
+
if (process.platform !== "win32") return null;
|
|
13729
|
+
for (const ext of (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")) if (ext && runnable(file + ext)) return file + ext;
|
|
13730
|
+
return null;
|
|
13358
13731
|
}
|
|
13359
|
-
|
|
13360
|
-
async function loadSchemaCatalog(cacheFile = join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "diffle", "schemastore.json"), request = fetch) {
|
|
13361
|
-
let cached = [];
|
|
13362
|
-
try {
|
|
13363
|
-
cached = schemaAssociations(JSON.parse(await readFile(cacheFile, "utf8")));
|
|
13364
|
-
if (cached.length && Date.now() - (await stat$1(cacheFile)).mtimeMs < MAX_AGE) return cached;
|
|
13365
|
-
} catch {}
|
|
13732
|
+
function runnable(file) {
|
|
13366
13733
|
try {
|
|
13367
|
-
|
|
13368
|
-
if (
|
|
13369
|
-
|
|
13370
|
-
|
|
13371
|
-
await writeFileAtomic(cacheFile, JSON.stringify({ schemas }) + "\n").catch(() => {});
|
|
13372
|
-
return schemas;
|
|
13734
|
+
if (!statSync(file).isFile()) return false;
|
|
13735
|
+
if (process.platform === "win32") return true;
|
|
13736
|
+
accessSync(file, constants.X_OK);
|
|
13737
|
+
return true;
|
|
13373
13738
|
} catch {
|
|
13374
|
-
return
|
|
13739
|
+
return false;
|
|
13375
13740
|
}
|
|
13376
13741
|
}
|
|
13377
|
-
/** Default schema settings for the languages routed to a process. */
|
|
13378
|
-
async function settingsFor(languages, command) {
|
|
13379
|
-
const settings = {};
|
|
13380
|
-
if (languages.includes("yaml")) settings.yaml = {
|
|
13381
|
-
hover: true,
|
|
13382
|
-
schemaStore: {
|
|
13383
|
-
enable: true,
|
|
13384
|
-
url: SCHEMA_CATALOG
|
|
13385
|
-
}
|
|
13386
|
-
};
|
|
13387
|
-
const program = argv0(command).split(/[\\/]/).pop()?.replace(/\.(exe|cmd|bat)$/i, "");
|
|
13388
|
-
if (languages.includes("toml") && program === "taplo") settings.evenBetterToml = { schema: {
|
|
13389
|
-
enabled: true,
|
|
13390
|
-
catalogs: [SCHEMA_CATALOG]
|
|
13391
|
-
} };
|
|
13392
|
-
if (languages.some((l) => l === "json" || l === "jsonc")) settings.json = { schemas: await loadSchemaCatalog() };
|
|
13393
|
-
return settings;
|
|
13394
|
-
}
|
|
13395
13742
|
/**
|
|
13396
|
-
*
|
|
13397
|
-
*
|
|
13398
|
-
*
|
|
13399
|
-
*
|
|
13743
|
+
* The file `executable` finally runs, as far as the filesystem tells: a pixi global
|
|
13744
|
+
* trampoline is replaced by the program its configuration names (the binary on PATH
|
|
13745
|
+
* is a launcher, with `trampoline_configuration/<name>.json` beside it pointing into
|
|
13746
|
+
* the environment), then symlinks are resolved. A path that leads nowhere is returned
|
|
13747
|
+
* as is.
|
|
13400
13748
|
*/
|
|
13401
|
-
function
|
|
13402
|
-
|
|
13403
|
-
|
|
13404
|
-
|
|
13405
|
-
|
|
13406
|
-
|
|
13407
|
-
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
|
|
13411
|
-
return
|
|
13412
|
-
}
|
|
13749
|
+
function realExecutable(executable) {
|
|
13750
|
+
let target = executable;
|
|
13751
|
+
try {
|
|
13752
|
+
const config = join(dirname(executable), "trampoline_configuration", `${basename(executable)}.json`);
|
|
13753
|
+
const { exe } = JSON.parse(readFileSync(config, "utf8"));
|
|
13754
|
+
if (typeof exe === "string" && exe) target = exe;
|
|
13755
|
+
} catch {}
|
|
13756
|
+
try {
|
|
13757
|
+
return realpathSync(target);
|
|
13758
|
+
} catch {
|
|
13759
|
+
return target;
|
|
13760
|
+
}
|
|
13413
13761
|
}
|
|
13414
13762
|
//#endregion
|
|
13415
|
-
//#region src/server/lsp/
|
|
13416
|
-
/** The server is off, still starting, or
|
|
13763
|
+
//#region src/server/lsp/LspProcess.ts
|
|
13764
|
+
/** The server is off, still starting, gone, or does not offer what was asked. Routes answer 409. */
|
|
13417
13765
|
var LspUnavailableError = class extends Error {};
|
|
13418
|
-
/**
|
|
13419
|
-
|
|
13420
|
-
|
|
13421
|
-
|
|
13422
|
-
|
|
13423
|
-
|
|
13424
|
-
|
|
13425
|
-
"type",
|
|
13426
|
-
"class",
|
|
13427
|
-
"enum",
|
|
13428
|
-
"interface",
|
|
13429
|
-
"struct",
|
|
13430
|
-
"typeParameter",
|
|
13431
|
-
"parameter",
|
|
13432
|
-
"variable",
|
|
13433
|
-
"property",
|
|
13434
|
-
"enumMember",
|
|
13435
|
-
"event",
|
|
13436
|
-
"function",
|
|
13437
|
-
"method",
|
|
13438
|
-
"macro",
|
|
13439
|
-
"keyword",
|
|
13440
|
-
"modifier",
|
|
13441
|
-
"comment",
|
|
13442
|
-
"string",
|
|
13443
|
-
"number",
|
|
13444
|
-
"regexp",
|
|
13445
|
-
"operator",
|
|
13446
|
-
"decorator"
|
|
13447
|
-
];
|
|
13766
|
+
/** The server never advertised the feature, in `initialize` or since. Advisory requests answer nothing instead. */
|
|
13767
|
+
var LspUnsupportedError = class extends LspUnavailableError {};
|
|
13768
|
+
const RegistrationsSchema = object({ registrations: array(object({
|
|
13769
|
+
id: string(),
|
|
13770
|
+
method: string()
|
|
13771
|
+
})) });
|
|
13772
|
+
const UnregistrationsSchema = object({ unregisterations: array(object({ id: string() })) });
|
|
13448
13773
|
const INIT_TIMEOUT_MS = 6e4;
|
|
13449
|
-
/**
|
|
13450
|
-
* Navigation opens whatever file a query lands in. Without a cap every path
|
|
13451
|
-
* visited in a long review stays open in the server, growing its memory and
|
|
13452
|
-
* per-save recheck cost.
|
|
13453
|
-
*/
|
|
13454
|
-
const MAX_UNTRACKED_OPEN = 64;
|
|
13455
|
-
/** Concurrent snapshot reads while mapping a result set back to paths. */
|
|
13456
|
-
const READ_CONCURRENCY = 8;
|
|
13457
|
-
/** Files outside the snapshot remembered for `readExternal`; the least recently named is forgotten first. */
|
|
13458
|
-
const MAX_EXTERNAL = 1024;
|
|
13459
13774
|
/** Graceful shutdown budget. Both together stay under the CLI's hard exit deadline (main.ts). */
|
|
13460
13775
|
const SHUTDOWN_RPC_MS = 500;
|
|
13461
13776
|
const SIGKILL_AFTER_MS = 500;
|
|
13462
|
-
/** LSP `ContentModified`: the document moved under the request, so the answer would be stale. */
|
|
13463
|
-
const CONTENT_MODIFIED = -32801;
|
|
13464
|
-
/** Waits before re-asking a request the server refused as stale, one per attempt. */
|
|
13465
|
-
const CONTENT_MODIFIED_BACKOFF_MS = [40, 120];
|
|
13466
13777
|
/**
|
|
13467
|
-
*
|
|
13468
|
-
*
|
|
13469
|
-
*
|
|
13470
|
-
|
|
13778
|
+
* `shell: true` runs the command under `sh -c`, so a plain kill would stop the
|
|
13779
|
+
* shell and orphan the language server. On POSIX the child leads its own process
|
|
13780
|
+
* group (`detached`), and shutdown signals the whole group.
|
|
13781
|
+
*/
|
|
13782
|
+
const DETACH = process.platform !== "win32";
|
|
13783
|
+
/**
|
|
13784
|
+
* One language server process: the only module that spawns one. Runs the
|
|
13785
|
+
* `initialize` handshake, keeps the status (with what `ServerOutput` reports), and
|
|
13786
|
+
* shuts it down. Speaks no document or query protocol: that is `Documents` and
|
|
13787
|
+
* `LspBridge`. Never throws from the constructor; failures become a status the UI
|
|
13788
|
+
* can show.
|
|
13471
13789
|
*/
|
|
13472
|
-
var
|
|
13790
|
+
var LspProcess = class {
|
|
13473
13791
|
opts;
|
|
13474
13792
|
child = null;
|
|
13475
13793
|
rpc = null;
|
|
13476
13794
|
current;
|
|
13477
13795
|
ready;
|
|
13478
|
-
stderr = "";
|
|
13479
|
-
progress = /* @__PURE__ */ new Map();
|
|
13480
|
-
/** Open documents, least recently synced first. */
|
|
13481
|
-
open = /* @__PURE__ */ new Map();
|
|
13482
|
-
/** Syncs in flight by path, so a track pass and a query racing on one file send a single didOpen. */
|
|
13483
|
-
syncing = /* @__PURE__ */ new Map();
|
|
13484
|
-
/** Paths `track` opened on the snapshot's behalf; closed again when they leave the snapshot. */
|
|
13485
|
-
tracked = /* @__PURE__ */ new Set();
|
|
13486
|
-
tracking = Promise.resolve();
|
|
13487
13796
|
closing = false;
|
|
13488
|
-
ownsGroup = false;
|
|
13489
|
-
/**
|
|
13490
|
-
|
|
13491
|
-
|
|
13492
|
-
|
|
13493
|
-
|
|
13494
|
-
|
|
13495
|
-
|
|
13496
|
-
|
|
13497
|
-
|
|
13498
|
-
|
|
13499
|
-
|
|
13500
|
-
|
|
13501
|
-
|
|
13502
|
-
|
|
13503
|
-
|
|
13504
|
-
|
|
13505
|
-
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
|
|
13509
|
-
|
|
13510
|
-
return this.current;
|
|
13511
|
-
}
|
|
13512
|
-
async definition(pos) {
|
|
13513
|
-
const result = await this.query("textDocument/definition", {
|
|
13514
|
-
textDocument: { uri: this.uri(pos.path) },
|
|
13515
|
-
position: toLsp(pos)
|
|
13516
|
-
}, pos.path);
|
|
13517
|
-
return this.toLocations(normalizeLocations(result));
|
|
13518
|
-
}
|
|
13519
|
-
async typeDefinition(pos) {
|
|
13520
|
-
const result = await this.query("textDocument/typeDefinition", {
|
|
13521
|
-
textDocument: { uri: this.uri(pos.path) },
|
|
13522
|
-
position: toLsp(pos)
|
|
13523
|
-
}, pos.path);
|
|
13524
|
-
return this.toLocations(normalizeLocations(result));
|
|
13525
|
-
}
|
|
13526
|
-
async hover(pos) {
|
|
13527
|
-
const result = await this.query("textDocument/hover", {
|
|
13528
|
-
textDocument: { uri: this.uri(pos.path) },
|
|
13529
|
-
position: toLsp(pos)
|
|
13530
|
-
}, pos.path);
|
|
13531
|
-
const md = result ? hoverMarkdown(result.contents) : null;
|
|
13532
|
-
const res = { contents: md == null ? null : localizeFileLinks(md, await this.resolveLinks(md)) };
|
|
13533
|
-
if (result?.range) {
|
|
13534
|
-
const { start, end } = result.range;
|
|
13535
|
-
res.range = {
|
|
13536
|
-
line: start.line + 1,
|
|
13537
|
-
col: start.character,
|
|
13538
|
-
endLine: end.line + 1,
|
|
13539
|
-
endCol: end.character
|
|
13540
|
-
};
|
|
13541
|
-
}
|
|
13542
|
-
return res;
|
|
13543
|
-
}
|
|
13544
|
-
/**
|
|
13545
|
-
* The semantic token class at a position, via `textDocument/semanticTokens/range`
|
|
13546
|
-
* on that one line. A server without semantic tokens, or a position between
|
|
13547
|
-
* tokens, yields null, so callers treat null as "no opinion".
|
|
13548
|
-
*/
|
|
13549
|
-
async tokenKind(pos) {
|
|
13550
|
-
const line = Math.max(0, pos.line - 1);
|
|
13551
|
-
let data;
|
|
13552
|
-
try {
|
|
13553
|
-
data = (await this.query("textDocument/semanticTokens/range", {
|
|
13554
|
-
textDocument: { uri: this.uri(pos.path) },
|
|
13555
|
-
range: {
|
|
13556
|
-
start: {
|
|
13557
|
-
line,
|
|
13558
|
-
character: 0
|
|
13559
|
-
},
|
|
13560
|
-
end: {
|
|
13561
|
-
line: line + 1,
|
|
13562
|
-
character: 0
|
|
13563
|
-
}
|
|
13564
|
-
}
|
|
13565
|
-
}, pos.path))?.data ?? [];
|
|
13566
|
-
} catch (e) {
|
|
13567
|
-
if (e instanceof LspUnavailableError) throw e;
|
|
13568
|
-
return { kind: null };
|
|
13569
|
-
}
|
|
13570
|
-
let tokLine = 0;
|
|
13571
|
-
let tokStart = 0;
|
|
13572
|
-
for (let i = 0; i + 4 < data.length; i += 5) {
|
|
13573
|
-
const deltaLine = data[i];
|
|
13574
|
-
tokLine += deltaLine;
|
|
13575
|
-
tokStart = deltaLine === 0 ? tokStart + data[i + 1] : data[i + 1];
|
|
13576
|
-
if (tokLine !== line || pos.col < tokStart || pos.col >= tokStart + data[i + 2]) continue;
|
|
13577
|
-
return { kind: this.tokenTypes[data[i + 3]] ?? null };
|
|
13578
|
-
}
|
|
13579
|
-
return { kind: null };
|
|
13580
|
-
}
|
|
13581
|
-
/**
|
|
13582
|
-
* Where else the symbol at `pos` occurs in its file, via `textDocument/documentHighlight`,
|
|
13583
|
-
* so scope and shadowing are the server's call. A server without the method, or a
|
|
13584
|
-
* position holding no symbol, yields no occurrences: the answer is advisory.
|
|
13585
|
-
*/
|
|
13586
|
-
async occurrences(pos) {
|
|
13587
|
-
let result;
|
|
13588
|
-
try {
|
|
13589
|
-
result = await this.query("textDocument/documentHighlight", {
|
|
13590
|
-
textDocument: { uri: this.uri(pos.path) },
|
|
13591
|
-
position: toLsp(pos)
|
|
13592
|
-
}, pos.path);
|
|
13593
|
-
} catch (e) {
|
|
13594
|
-
if (e instanceof LspUnavailableError) throw e;
|
|
13595
|
-
return { occurrences: [] };
|
|
13596
|
-
}
|
|
13597
|
-
return { occurrences: (result ?? []).map(({ range: { start, end }, kind }) => ({
|
|
13598
|
-
line: start.line + 1,
|
|
13599
|
-
col: start.character,
|
|
13600
|
-
endLine: end.line + 1,
|
|
13601
|
-
endCol: end.character,
|
|
13602
|
-
kind: kind === 3 ? "write" : kind === 2 ? "read" : "text"
|
|
13603
|
-
})) };
|
|
13797
|
+
ownsGroup = false;
|
|
13798
|
+
/** What the server reports; each change lands on the status unless the process is done. */
|
|
13799
|
+
output = new ServerOutput((change) => {
|
|
13800
|
+
if (!this.closing && this.current.state !== "unavailable") this.set({
|
|
13801
|
+
...this.current,
|
|
13802
|
+
...change
|
|
13803
|
+
});
|
|
13804
|
+
});
|
|
13805
|
+
/** Features the server offers, from `initialize` and from registrations since. */
|
|
13806
|
+
advertised = /* @__PURE__ */ new Set();
|
|
13807
|
+
/** Dynamic registrations by their id, so an unregistration knows which feature it ends. */
|
|
13808
|
+
registrations = /* @__PURE__ */ new Map();
|
|
13809
|
+
constructor(opts) {
|
|
13810
|
+
this.opts = opts;
|
|
13811
|
+
this.current = {
|
|
13812
|
+
name: programOf(opts.command),
|
|
13813
|
+
command: opts.command,
|
|
13814
|
+
state: "starting",
|
|
13815
|
+
log: this.output.summary()
|
|
13816
|
+
};
|
|
13817
|
+
this.ready = this.launch();
|
|
13818
|
+
this.ready.catch(() => {});
|
|
13604
13819
|
}
|
|
13605
|
-
|
|
13606
|
-
|
|
13607
|
-
|
|
13608
|
-
position: toLsp(pos),
|
|
13609
|
-
context: { includeDeclaration: opts.includeDeclaration ?? true }
|
|
13610
|
-
}, pos.path);
|
|
13611
|
-
return this.toLocations(normalizeLocations(result));
|
|
13820
|
+
/** How messages name the server: `basedpyright-langserver --stdio` → `basedpyright-langserver`. */
|
|
13821
|
+
get name() {
|
|
13822
|
+
return this.current.name;
|
|
13612
13823
|
}
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
* Servers index the workspace lazily and cap how much of it they index
|
|
13616
|
-
* (pyrefly: in the background from the first open, 2000 files), so a
|
|
13617
|
-
* reference in a changed file that nobody queried yet is otherwise only found
|
|
13618
|
-
* once its file happens to be indexed, and from the text on disk. Opening
|
|
13619
|
-
* everything at start also warms the index before the first query. Calls are
|
|
13620
|
-
* serialized; the last set wins. Never throws: a path the snapshot cannot read
|
|
13621
|
-
* is skipped.
|
|
13622
|
-
*/
|
|
13623
|
-
track(paths) {
|
|
13624
|
-
const run = this.tracking.then(() => this.trackNow(paths)).catch(() => {});
|
|
13625
|
-
this.tracking = run;
|
|
13626
|
-
return run;
|
|
13824
|
+
status() {
|
|
13825
|
+
return this.current;
|
|
13627
13826
|
}
|
|
13628
|
-
|
|
13629
|
-
|
|
13630
|
-
|
|
13631
|
-
for (const path of this.tracked) {
|
|
13632
|
-
if (next.has(path)) continue;
|
|
13633
|
-
this.tracked.delete(path);
|
|
13634
|
-
if (this.open.delete(path)) rpc.notify("textDocument/didClose", { textDocument: { uri: this.uri(path) } });
|
|
13635
|
-
}
|
|
13636
|
-
for (const path of next) try {
|
|
13637
|
-
await this.sync(path);
|
|
13638
|
-
this.tracked.add(path);
|
|
13639
|
-
} catch (e) {
|
|
13640
|
-
if (e instanceof LspUnavailableError) return;
|
|
13641
|
-
}
|
|
13827
|
+
/** The retained log lines, oldest first. */
|
|
13828
|
+
entries() {
|
|
13829
|
+
return this.output.entries();
|
|
13642
13830
|
}
|
|
13643
|
-
|
|
13644
|
-
|
|
13645
|
-
|
|
13646
|
-
const out = [];
|
|
13647
|
-
if (isDocumentSymbols(result)) {
|
|
13648
|
-
const walk = (syms, container) => {
|
|
13649
|
-
for (const s of syms) {
|
|
13650
|
-
const start = (s.selectionRange ?? s.range).start;
|
|
13651
|
-
const endLine = Math.max(start.line, (s.range ?? s.selectionRange).end.line) + 1;
|
|
13652
|
-
out.push({
|
|
13653
|
-
name: s.name,
|
|
13654
|
-
kind: s.kind,
|
|
13655
|
-
container,
|
|
13656
|
-
path,
|
|
13657
|
-
line: start.line + 1,
|
|
13658
|
-
endLine,
|
|
13659
|
-
col: start.character
|
|
13660
|
-
});
|
|
13661
|
-
if (s.children?.length) walk(s.children, s.name);
|
|
13662
|
-
}
|
|
13663
|
-
};
|
|
13664
|
-
walk(result);
|
|
13665
|
-
return out;
|
|
13666
|
-
}
|
|
13667
|
-
for (const s of result) {
|
|
13668
|
-
const start = "range" in s.location ? s.location.range.start : {
|
|
13669
|
-
line: 0,
|
|
13670
|
-
character: 0
|
|
13671
|
-
};
|
|
13672
|
-
const end = "range" in s.location ? s.location.range.end : start;
|
|
13673
|
-
out.push({
|
|
13674
|
-
name: s.name,
|
|
13675
|
-
kind: s.kind,
|
|
13676
|
-
container: s.containerName || void 0,
|
|
13677
|
-
path,
|
|
13678
|
-
line: start.line + 1,
|
|
13679
|
-
endLine: Math.max(start.line, end.line) + 1,
|
|
13680
|
-
col: start.character
|
|
13681
|
-
});
|
|
13682
|
-
}
|
|
13683
|
-
return out;
|
|
13831
|
+
/** Whether the server advertised `feature`, at `initialize` or by registering it since. False until initialized. */
|
|
13832
|
+
supports(feature) {
|
|
13833
|
+
return this.advertised.has(feature);
|
|
13684
13834
|
}
|
|
13685
|
-
|
|
13686
|
-
|
|
13687
|
-
* exposes, best `fuzzyRank` match first. Membership comes from `has`, never from reading the file: a
|
|
13688
|
-
* per-keystroke search must not read every hit's text.
|
|
13689
|
-
*/
|
|
13690
|
-
async workspaceSymbols(query, limit = 200) {
|
|
13691
|
-
const result = await this.query("workspace/symbol", { query });
|
|
13692
|
-
if (!result) return [];
|
|
13693
|
-
const paths = await Promise.all(result.map((s) => this.pathOf(s.location.uri)));
|
|
13694
|
-
const inside = await this.membership(paths);
|
|
13695
|
-
const out = [];
|
|
13696
|
-
const seen = /* @__PURE__ */ new Set();
|
|
13697
|
-
result.forEach((s, i) => {
|
|
13698
|
-
const path = paths[i];
|
|
13699
|
-
if (path == null || !inside.get(path)) return;
|
|
13700
|
-
const start = "range" in s.location ? s.location.range.start : {
|
|
13701
|
-
line: 0,
|
|
13702
|
-
character: 0
|
|
13703
|
-
};
|
|
13704
|
-
const key = `${path}:${start.line}:${start.character}:${s.name}`;
|
|
13705
|
-
if (seen.has(key)) return;
|
|
13706
|
-
seen.add(key);
|
|
13707
|
-
const end = "range" in s.location ? s.location.range.end : start;
|
|
13708
|
-
out.push({
|
|
13709
|
-
name: s.name,
|
|
13710
|
-
kind: s.kind,
|
|
13711
|
-
container: s.containerName || void 0,
|
|
13712
|
-
path,
|
|
13713
|
-
line: start.line + 1,
|
|
13714
|
-
endLine: Math.max(start.line, end.line) + 1,
|
|
13715
|
-
col: start.character
|
|
13716
|
-
});
|
|
13717
|
-
});
|
|
13718
|
-
return fuzzyRank(out, query, (s) => s.name).slice(0, limit);
|
|
13835
|
+
features() {
|
|
13836
|
+
return [...this.advertised];
|
|
13719
13837
|
}
|
|
13720
|
-
/**
|
|
13721
|
-
|
|
13722
|
-
|
|
13723
|
-
|
|
13724
|
-
* never returned stays behind the snapshot allowlist.
|
|
13725
|
-
*/
|
|
13726
|
-
async readExternal(path) {
|
|
13727
|
-
if (!this.external.has(path)) return null;
|
|
13728
|
-
return readFile(path).catch(() => null);
|
|
13838
|
+
/** The background work the server reports, as a reason an empty answer may be incomplete; null when idle. */
|
|
13839
|
+
busy() {
|
|
13840
|
+
const work = this.current.activity?.length ? this.current.activity.join("; ") : null;
|
|
13841
|
+
return work ? `${this.name} is busy: ${work}; try again when it finishes` : null;
|
|
13729
13842
|
}
|
|
13730
|
-
/**
|
|
13731
|
-
async
|
|
13732
|
-
|
|
13733
|
-
|
|
13734
|
-
|
|
13843
|
+
/** The connection once initialized; throws LspUnavailableError when the server is gone. */
|
|
13844
|
+
async connection() {
|
|
13845
|
+
try {
|
|
13846
|
+
await this.ready;
|
|
13847
|
+
} catch {
|
|
13848
|
+
throw new LspUnavailableError(this.current.message ?? "language server unavailable");
|
|
13849
|
+
}
|
|
13850
|
+
if (this.current.state !== "ready" || !this.rpc) throw new LspUnavailableError(this.current.message ?? "language server unavailable");
|
|
13851
|
+
return this.rpc;
|
|
13735
13852
|
}
|
|
13736
13853
|
async close() {
|
|
13737
13854
|
this.closing = true;
|
|
@@ -13790,18 +13907,12 @@ var LspBridge = class LspBridge {
|
|
|
13790
13907
|
throw e;
|
|
13791
13908
|
}
|
|
13792
13909
|
this.child = child;
|
|
13793
|
-
child.stderr?.on("data", (d) =>
|
|
13794
|
-
const chunk = d.toString("utf8");
|
|
13795
|
-
this.stderr = (this.stderr + chunk).slice(-2e3);
|
|
13796
|
-
if (!this.closing && this.current.state !== "unavailable") this.set({
|
|
13797
|
-
...this.current,
|
|
13798
|
-
stderr: this.stderr.trim()
|
|
13799
|
-
});
|
|
13800
|
-
});
|
|
13910
|
+
child.stderr?.on("data", (d) => this.output.stderr(d.toString("utf8")));
|
|
13801
13911
|
child.on("error", (e) => this.fail(`cannot start "${this.opts.command}": ${e.message}`));
|
|
13802
13912
|
child.on("exit", (code, signal) => {
|
|
13803
13913
|
if (this.closing) return;
|
|
13804
|
-
this.
|
|
13914
|
+
this.output.stderr("\n");
|
|
13915
|
+
this.fail(`"${this.opts.command}" exited (${signal ?? code})`);
|
|
13805
13916
|
});
|
|
13806
13917
|
if (!child.stdin || !child.stdout) {
|
|
13807
13918
|
this.fail(`cannot start "${this.opts.command}": no stdio`);
|
|
@@ -13809,11 +13920,15 @@ var LspBridge = class LspBridge {
|
|
|
13809
13920
|
}
|
|
13810
13921
|
const rpc = new JsonRpcConnection(child.stdout, child.stdin);
|
|
13811
13922
|
this.rpc = rpc;
|
|
13812
|
-
rpc.
|
|
13923
|
+
rpc.onMessage((method, params) => {
|
|
13924
|
+
if (!this.closing && this.current.state !== "unavailable") this.output.message(method, params);
|
|
13925
|
+
});
|
|
13813
13926
|
rpc.onRequest("workspace/configuration", async (params) => configurationItems(await this.opts.settings ?? {}, params));
|
|
13927
|
+
rpc.onRequest("client/registerCapability", (params) => this.register(params));
|
|
13928
|
+
rpc.onRequest("client/unregisterCapability", (params) => this.unregister(params));
|
|
13814
13929
|
const rootUri = pathToFileURL(this.opts.root).href;
|
|
13815
13930
|
try {
|
|
13816
|
-
const
|
|
13931
|
+
const capabilities = (await rpc.request("initialize", {
|
|
13817
13932
|
processId: process.pid,
|
|
13818
13933
|
clientInfo: { name: "diffle" },
|
|
13819
13934
|
rootUri,
|
|
@@ -13822,31 +13937,10 @@ var LspBridge = class LspBridge {
|
|
|
13822
13937
|
uri: rootUri,
|
|
13823
13938
|
name: "workspace"
|
|
13824
13939
|
}],
|
|
13825
|
-
|
|
13826
|
-
|
|
13827
|
-
|
|
13828
|
-
|
|
13829
|
-
definition: { linkSupport: true },
|
|
13830
|
-
typeDefinition: { linkSupport: true },
|
|
13831
|
-
references: {},
|
|
13832
|
-
documentHighlight: {},
|
|
13833
|
-
hover: { contentFormat: ["markdown", "plaintext"] },
|
|
13834
|
-
documentSymbol: { hierarchicalDocumentSymbolSupport: true },
|
|
13835
|
-
semanticTokens: {
|
|
13836
|
-
requests: { range: true },
|
|
13837
|
-
tokenTypes: DEFAULT_TOKEN_TYPES,
|
|
13838
|
-
tokenModifiers: [],
|
|
13839
|
-
formats: ["relative"]
|
|
13840
|
-
}
|
|
13841
|
-
},
|
|
13842
|
-
workspace: {
|
|
13843
|
-
symbol: {},
|
|
13844
|
-
workspaceFolders: true,
|
|
13845
|
-
configuration: true
|
|
13846
|
-
}
|
|
13847
|
-
}
|
|
13848
|
-
}, INIT_TIMEOUT_MS))?.capabilities?.semanticTokensProvider?.legend?.tokenTypes;
|
|
13849
|
-
if (legend?.length) this.tokenTypes = legend;
|
|
13940
|
+
initializationOptions: this.opts.initializationOptions,
|
|
13941
|
+
capabilities: CLIENT_CAPABILITIES
|
|
13942
|
+
}, INIT_TIMEOUT_MS))?.capabilities ?? {};
|
|
13943
|
+
for (const [feature, { provider }] of Object.entries(FEATURES)) if (capabilities[provider]) this.advertised.add(feature);
|
|
13850
13944
|
rpc.notify("initialized", {});
|
|
13851
13945
|
if (this.opts.settings) {
|
|
13852
13946
|
const settings = await this.opts.settings;
|
|
@@ -13861,40 +13955,30 @@ var LspBridge = class LspBridge {
|
|
|
13861
13955
|
state: "ready"
|
|
13862
13956
|
});
|
|
13863
13957
|
}
|
|
13864
|
-
|
|
13865
|
-
|
|
13866
|
-
const
|
|
13867
|
-
if (
|
|
13868
|
-
|
|
13869
|
-
const
|
|
13870
|
-
|
|
13871
|
-
|
|
13872
|
-
|
|
13873
|
-
|
|
13874
|
-
|
|
13875
|
-
|
|
13876
|
-
|
|
13877
|
-
|
|
13958
|
+
/** A feature the server offers from now on, as an editor takes it: counted like one advertised at `initialize`. */
|
|
13959
|
+
register(params) {
|
|
13960
|
+
const request = RegistrationsSchema.safeParse(params);
|
|
13961
|
+
if (!request.success) throw new JsonRpcError(-32602, "Invalid client/registerCapability parameters");
|
|
13962
|
+
for (const { id, method } of request.data.registrations) {
|
|
13963
|
+
const feature = featureOf(method);
|
|
13964
|
+
if (!feature) continue;
|
|
13965
|
+
this.registrations.set(id, feature);
|
|
13966
|
+
this.advertised.add(feature);
|
|
13967
|
+
}
|
|
13968
|
+
this.opts.onStatus(this.current);
|
|
13969
|
+
return null;
|
|
13970
|
+
}
|
|
13971
|
+
unregister(params) {
|
|
13972
|
+
const request = UnregistrationsSchema.safeParse(params);
|
|
13973
|
+
if (!request.success) throw new JsonRpcError(-32602, "Invalid client/unregisterCapability parameters");
|
|
13974
|
+
for (const { id } of request.data.unregisterations) {
|
|
13975
|
+
const feature = this.registrations.get(id);
|
|
13976
|
+
if (!feature) continue;
|
|
13977
|
+
this.registrations.delete(id);
|
|
13978
|
+
this.advertised.delete(feature);
|
|
13878
13979
|
}
|
|
13879
|
-
|
|
13880
|
-
|
|
13881
|
-
const { kind, title, message, percentage } = p.value;
|
|
13882
|
-
const percent = typeof percentage === "number" && percentage >= 0 && percentage <= 100 ? percentage : void 0;
|
|
13883
|
-
if (kind === "begin" && typeof title === "string") this.progress.set(p.token, {
|
|
13884
|
-
title,
|
|
13885
|
-
message: typeof message === "string" ? message : void 0,
|
|
13886
|
-
percentage: percent
|
|
13887
|
-
});
|
|
13888
|
-
else if (kind === "report" && this.progress.has(p.token)) {
|
|
13889
|
-
if (typeof message === "string") this.progress.get(p.token).message = message;
|
|
13890
|
-
if (percent !== void 0) this.progress.get(p.token).percentage = percent;
|
|
13891
|
-
} else if (kind === "end") {
|
|
13892
|
-
if (!this.progress.delete(p.token)) return;
|
|
13893
|
-
} else return;
|
|
13894
|
-
this.set({
|
|
13895
|
-
...this.current,
|
|
13896
|
-
activity: [...this.progress.values()].map((p) => `${p.title}${p.percentage == null ? "" : ` (${p.percentage}%)`}${p.message ? `: ${p.message}` : ""}`)
|
|
13897
|
-
});
|
|
13980
|
+
this.opts.onStatus(this.current);
|
|
13981
|
+
return null;
|
|
13898
13982
|
}
|
|
13899
13983
|
fail(message) {
|
|
13900
13984
|
if (this.current.state === "unavailable") return;
|
|
@@ -13902,75 +13986,94 @@ var LspBridge = class LspBridge {
|
|
|
13902
13986
|
name: this.current.name,
|
|
13903
13987
|
command: this.opts.command,
|
|
13904
13988
|
state: "unavailable",
|
|
13905
|
-
message
|
|
13989
|
+
message,
|
|
13990
|
+
log: this.output.summary()
|
|
13906
13991
|
});
|
|
13907
13992
|
this.rpc?.dispose(new LspUnavailableError(message));
|
|
13908
13993
|
}
|
|
13909
13994
|
set(status) {
|
|
13910
|
-
if (JSON.stringify(status) === JSON.stringify(this.current)) return;
|
|
13911
13995
|
this.current = status;
|
|
13912
13996
|
this.opts.onStatus(status);
|
|
13913
13997
|
}
|
|
13914
|
-
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
|
|
13920
|
-
|
|
13921
|
-
|
|
13922
|
-
|
|
13998
|
+
};
|
|
13999
|
+
//#endregion
|
|
14000
|
+
//#region src/server/lsp/Documents.ts
|
|
14001
|
+
/**
|
|
14002
|
+
* Navigation opens whatever file a query lands in. Without a cap every path
|
|
14003
|
+
* visited in a long review stays open in the server, growing its memory and
|
|
14004
|
+
* per-save recheck cost.
|
|
14005
|
+
*/
|
|
14006
|
+
const MAX_UNTRACKED_OPEN = 64;
|
|
14007
|
+
/**
|
|
14008
|
+
* The documents one server has open, kept in step with the snapshot: the diff's own files
|
|
14009
|
+
* for as long as they are in it, and the files queries land in until they fall off the
|
|
14010
|
+
* least-recently-used end. Sends `didOpen`, `didChange` and `didClose`; nothing else.
|
|
14011
|
+
*/
|
|
14012
|
+
var Documents = class {
|
|
14013
|
+
opts;
|
|
14014
|
+
/** Open documents, least recently synced first. */
|
|
14015
|
+
open = /* @__PURE__ */ new Map();
|
|
14016
|
+
/** Syncs in flight by path, so a track pass and a query racing on one file send a single didOpen. */
|
|
14017
|
+
syncing = /* @__PURE__ */ new Map();
|
|
14018
|
+
/** Paths `track` opened on the snapshot's behalf; closed again when they leave the snapshot. */
|
|
14019
|
+
tracked = /* @__PURE__ */ new Set();
|
|
14020
|
+
tracking = Promise.resolve();
|
|
14021
|
+
constructor(opts) {
|
|
14022
|
+
this.opts = opts;
|
|
13923
14023
|
}
|
|
13924
|
-
/**
|
|
13925
|
-
|
|
13926
|
-
|
|
13927
|
-
* `track` pass opening the diff — so it refuses to answer from text it no longer holds.
|
|
13928
|
-
* Re-syncing and asking again is what an editor does. Giving up reads as an outage, the
|
|
13929
|
-
* one thing the reader can act on, rather than as a failed route.
|
|
13930
|
-
*/
|
|
13931
|
-
async query(method, params, path) {
|
|
13932
|
-
for (let attempt = 0;; attempt++) {
|
|
13933
|
-
const rpc = path == null ? await this.ensureReady() : await this.sync(path);
|
|
13934
|
-
try {
|
|
13935
|
-
const pending = busyReason(this.current);
|
|
13936
|
-
const result = await rpc.request(method, params);
|
|
13937
|
-
if (result == null || Array.isArray(result) && !result.length) {
|
|
13938
|
-
const busy = busyReason(this.current) ?? pending;
|
|
13939
|
-
if (busy) throw new LspUnavailableError(busy);
|
|
13940
|
-
if (this.current.notice) {
|
|
13941
|
-
const { severity, message } = this.current.notice;
|
|
13942
|
-
throw new LspUnavailableError(`${this.current.name} returned no result; last server ${severity}: ${message}`);
|
|
13943
|
-
}
|
|
13944
|
-
}
|
|
13945
|
-
return result;
|
|
13946
|
-
} catch (e) {
|
|
13947
|
-
if (!(e instanceof JsonRpcError) || e.code !== CONTENT_MODIFIED) throw e;
|
|
13948
|
-
const backoff = CONTENT_MODIFIED_BACKOFF_MS[attempt];
|
|
13949
|
-
if (backoff == null) throw new LspUnavailableError(`${this.current.name} could not answer while ${path ?? "the repository"} kept changing; try again`);
|
|
13950
|
-
await new Promise((r) => setTimeout(r, backoff));
|
|
13951
|
-
}
|
|
13952
|
-
}
|
|
14024
|
+
/** The text the server holds for an open path. */
|
|
14025
|
+
textOf(path) {
|
|
14026
|
+
return this.open.get(path)?.text;
|
|
13953
14027
|
}
|
|
13954
14028
|
/**
|
|
13955
|
-
* Opens or updates the document so the server sees the snapshot's text before
|
|
13956
|
-
*
|
|
13957
|
-
*
|
|
13958
|
-
*
|
|
14029
|
+
* Opens or updates the document so the server sees the snapshot's text before a
|
|
14030
|
+
* request. Concurrent syncs of one path share a single read and notification.
|
|
14031
|
+
* Untracked documents beyond `maxOpen` are closed, least recently used first;
|
|
14032
|
+
* tracked ones stay open for the snapshot's lifetime.
|
|
13959
14033
|
*/
|
|
13960
|
-
|
|
13961
|
-
const rpc = await this.ensureReady();
|
|
14034
|
+
sync(path) {
|
|
13962
14035
|
let pending = this.syncing.get(path);
|
|
13963
14036
|
if (!pending) {
|
|
13964
|
-
pending = this.syncNow(
|
|
14037
|
+
pending = this.syncNow(path).finally(() => this.syncing.delete(path));
|
|
13965
14038
|
this.syncing.set(path, pending);
|
|
13966
14039
|
}
|
|
13967
|
-
|
|
13968
|
-
|
|
14040
|
+
return pending;
|
|
14041
|
+
}
|
|
14042
|
+
/**
|
|
14043
|
+
* Keeps the diff's own files open in the server with their snapshot text.
|
|
14044
|
+
* Servers index the workspace lazily and cap how much of it they index
|
|
14045
|
+
* (pyrefly: in the background from the first open, 2000 files), so a
|
|
14046
|
+
* reference in a changed file that nobody queried yet is otherwise only found
|
|
14047
|
+
* once its file happens to be indexed, and from the text on disk. Opening
|
|
14048
|
+
* everything at start also warms the index before the first query. Calls are
|
|
14049
|
+
* serialized; the last set wins. Never throws: a path the snapshot cannot read
|
|
14050
|
+
* is skipped.
|
|
14051
|
+
*/
|
|
14052
|
+
track(paths) {
|
|
14053
|
+
const run = this.tracking.then(() => this.trackNow(paths)).catch(() => {});
|
|
14054
|
+
this.tracking = run;
|
|
14055
|
+
return run;
|
|
14056
|
+
}
|
|
14057
|
+
async trackNow(paths) {
|
|
14058
|
+
const rpc = await this.opts.connection();
|
|
14059
|
+
const next = new Set(paths);
|
|
14060
|
+
for (const path of this.tracked) {
|
|
14061
|
+
if (next.has(path)) continue;
|
|
14062
|
+
this.tracked.delete(path);
|
|
14063
|
+
if (this.open.delete(path)) rpc.notify("textDocument/didClose", { textDocument: { uri: this.opts.uri(path) } });
|
|
14064
|
+
}
|
|
14065
|
+
for (const path of next) try {
|
|
14066
|
+
await this.sync(path);
|
|
14067
|
+
this.tracked.add(path);
|
|
14068
|
+
} catch (e) {
|
|
14069
|
+
if (e instanceof LspUnavailableError) return;
|
|
14070
|
+
}
|
|
13969
14071
|
}
|
|
13970
|
-
async syncNow(
|
|
14072
|
+
async syncNow(path) {
|
|
14073
|
+
const rpc = await this.opts.connection();
|
|
13971
14074
|
const text = await this.opts.read(path);
|
|
13972
14075
|
if (text == null) throw new Error(`${path} is not in the snapshot`);
|
|
13973
|
-
const uri = this.uri(path);
|
|
14076
|
+
const uri = this.opts.uri(path);
|
|
13974
14077
|
const doc = this.open.get(path);
|
|
13975
14078
|
if (!doc) {
|
|
13976
14079
|
this.open.set(path, {
|
|
@@ -14010,31 +14113,72 @@ var LspBridge = class LspBridge {
|
|
|
14010
14113
|
if (this.tracked.has(path) || this.syncing.has(path)) continue;
|
|
14011
14114
|
this.open.delete(path);
|
|
14012
14115
|
untracked--;
|
|
14013
|
-
rpc.notify("textDocument/didClose", { textDocument: { uri: this.uri(path) } });
|
|
14116
|
+
rpc.notify("textDocument/didClose", { textDocument: { uri: this.opts.uri(path) } });
|
|
14014
14117
|
}
|
|
14015
14118
|
}
|
|
14016
|
-
|
|
14017
|
-
|
|
14018
|
-
|
|
14019
|
-
|
|
14020
|
-
|
|
14021
|
-
|
|
14022
|
-
|
|
14023
|
-
|
|
14024
|
-
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
|
|
14028
|
-
|
|
14029
|
-
|
|
14030
|
-
|
|
14031
|
-
|
|
14032
|
-
|
|
14033
|
-
|
|
14034
|
-
|
|
14035
|
-
|
|
14036
|
-
|
|
14037
|
-
|
|
14119
|
+
};
|
|
14120
|
+
//#endregion
|
|
14121
|
+
//#region src/server/lsp/hover.ts
|
|
14122
|
+
/**
|
|
14123
|
+
* One markdown document for the tooltip, or null when the server sent nothing
|
|
14124
|
+
* worth showing. Plaintext and `{language, value}` parts become fenced blocks,
|
|
14125
|
+
* so their text is never re-read as markdown.
|
|
14126
|
+
*/
|
|
14127
|
+
function hoverMarkdown(contents) {
|
|
14128
|
+
if (contents == null) return null;
|
|
14129
|
+
const parts = (Array.isArray(contents) ? contents : [contents]).map(partMarkdown).filter((p) => p.trim() !== "");
|
|
14130
|
+
return parts.length ? parts.join("\n\n") : null;
|
|
14131
|
+
}
|
|
14132
|
+
function partMarkdown(part) {
|
|
14133
|
+
if (typeof part === "string") return part;
|
|
14134
|
+
if ("kind" in part) return part.kind === "markdown" ? part.value : fence(part.value, "");
|
|
14135
|
+
return fence(part.value, part.language);
|
|
14136
|
+
}
|
|
14137
|
+
/** A fence long enough that a backtick run inside the code cannot close it early. */
|
|
14138
|
+
function fence(code, lang) {
|
|
14139
|
+
const longest = Math.max(2, ...[...code.matchAll(/`+/g)].map((m) => m[0].length));
|
|
14140
|
+
const ticks = "`".repeat(longest + 1);
|
|
14141
|
+
return `${ticks}${lang}\n${code.replace(/\n$/, "")}\n${ticks}`;
|
|
14142
|
+
}
|
|
14143
|
+
/** An inline markdown link whose target is a `file:` URI, as pyright and pyrefly write "Go to X". */
|
|
14144
|
+
const fileLink = () => /\[([^\]]*)\]\(\s*<?(file:[^\s)>]+)>?\s*\)/g;
|
|
14145
|
+
/** The `file:` URIs the hover text links to, each once. */
|
|
14146
|
+
function fileLinkUris(md) {
|
|
14147
|
+
return [...new Set([...md.matchAll(fileLink())].map((m) => m[2]))];
|
|
14148
|
+
}
|
|
14149
|
+
/**
|
|
14150
|
+
* Rewrites the hover text's `file:` links to `diffle:<path>#L<line>`, which the client follows as an
|
|
14151
|
+
* in-app jump. `resolved` maps each URI to its repo-relative path and line; a URI it omits lies
|
|
14152
|
+
* outside the repository and becomes plain text, since opening a browser tab on a source file is
|
|
14153
|
+
* never what the reader wants.
|
|
14154
|
+
*/
|
|
14155
|
+
function localizeFileLinks(md, resolved) {
|
|
14156
|
+
return md.replace(fileLink(), (_m, label, uri) => {
|
|
14157
|
+
const loc = resolved.get(uri);
|
|
14158
|
+
if (!loc) return label;
|
|
14159
|
+
return `[${label}](diffle:${encodeURI(loc.path)}${loc.line != null ? `#L${loc.line}` : ""})`;
|
|
14160
|
+
});
|
|
14161
|
+
}
|
|
14162
|
+
//#endregion
|
|
14163
|
+
//#region src/server/lsp/Locator.ts
|
|
14164
|
+
/** Concurrent snapshot reads while mapping a result set back to paths. */
|
|
14165
|
+
const READ_CONCURRENCY = 8;
|
|
14166
|
+
/** Files outside the snapshot remembered for `readExternal`; the least recently named is forgotten first. */
|
|
14167
|
+
const MAX_EXTERNAL = 1024;
|
|
14168
|
+
/**
|
|
14169
|
+
* Maps the URIs a language server answers with back to snapshot paths, and remembers
|
|
14170
|
+
* the files outside the snapshot it named, so `/api/file` may serve those and nothing
|
|
14171
|
+
* else beyond the allowlist.
|
|
14172
|
+
*/
|
|
14173
|
+
var Locator = class {
|
|
14174
|
+
opts;
|
|
14175
|
+
/** Symlink-free root; servers often canonicalize, so URIs may not share the root's spelling. */
|
|
14176
|
+
realRoot;
|
|
14177
|
+
/** Absolute paths of files outside the snapshot that results named, least recently first. */
|
|
14178
|
+
external = /* @__PURE__ */ new Set();
|
|
14179
|
+
constructor(opts) {
|
|
14180
|
+
this.opts = opts;
|
|
14181
|
+
this.realRoot = safeRealpathSync(opts.root);
|
|
14038
14182
|
}
|
|
14039
14183
|
/**
|
|
14040
14184
|
* Repo-relative path for a file URI inside the root; null otherwise. Compares
|
|
@@ -14055,13 +14199,13 @@ var LspBridge = class LspBridge {
|
|
|
14055
14199
|
return relativeInside(this.realRoot, real);
|
|
14056
14200
|
}
|
|
14057
14201
|
/**
|
|
14058
|
-
*
|
|
14059
|
-
*
|
|
14060
|
-
*
|
|
14061
|
-
*
|
|
14062
|
-
*
|
|
14202
|
+
* Server locations as paths with the text of each line: a snapshot path, or, for a
|
|
14203
|
+
* file the snapshot lacks (stdlib, site-packages, an ignored venv), the file itself
|
|
14204
|
+
* read from disk and marked external. Each distinct URI is read once, in parallel
|
|
14205
|
+
* with a limit, so a large reference set costs a bounded number of reads. A file
|
|
14206
|
+
* readable neither way is dropped.
|
|
14063
14207
|
*/
|
|
14064
|
-
async
|
|
14208
|
+
async locations(locs) {
|
|
14065
14209
|
const uris = [...new Set(locs.map((l) => l.uri))];
|
|
14066
14210
|
const found = await mapLimit(uris, READ_CONCURRENCY, (uri) => this.locate(uri));
|
|
14067
14211
|
const byUri = new Map(uris.map((uri, i) => [uri, found[i]]));
|
|
@@ -14079,12 +14223,48 @@ var LspBridge = class LspBridge {
|
|
|
14079
14223
|
if (f.external) loc.external = true;
|
|
14080
14224
|
out.push(loc);
|
|
14081
14225
|
}
|
|
14082
|
-
return
|
|
14226
|
+
return out;
|
|
14227
|
+
}
|
|
14228
|
+
/**
|
|
14229
|
+
* Where each `file:` link in hover text points inside the repository: the repo-relative path and,
|
|
14230
|
+
* from the URI's `#line` or `#line,col` fragment, the 1-based line. Links outside the root are
|
|
14231
|
+
* left out, so the client never offers a jump it cannot make.
|
|
14232
|
+
*/
|
|
14233
|
+
async links(md) {
|
|
14234
|
+
const uris = fileLinkUris(md);
|
|
14235
|
+
const out = /* @__PURE__ */ new Map();
|
|
14236
|
+
await Promise.all(uris.map(async (uri) => {
|
|
14237
|
+
const hash = uri.indexOf("#");
|
|
14238
|
+
const path = await this.pathOf(hash === -1 ? uri : uri.slice(0, hash));
|
|
14239
|
+
if (path == null) return;
|
|
14240
|
+
const line = hash === -1 ? null : /^L?(\d+)/.exec(uri.slice(hash + 1))?.[1];
|
|
14241
|
+
out.set(uri, line == null ? { path } : {
|
|
14242
|
+
path,
|
|
14243
|
+
line: Number(line)
|
|
14244
|
+
});
|
|
14245
|
+
}));
|
|
14246
|
+
return out;
|
|
14247
|
+
}
|
|
14248
|
+
/** `has` for each distinct path, in parallel. */
|
|
14249
|
+
async membership(paths) {
|
|
14250
|
+
const distinct = [...new Set(paths.filter((p) => p != null))];
|
|
14251
|
+
const flags = await mapLimit(distinct, READ_CONCURRENCY, (p) => this.opts.has(p));
|
|
14252
|
+
return new Map(distinct.map((p, i) => [p, flags[i]]));
|
|
14253
|
+
}
|
|
14254
|
+
/**
|
|
14255
|
+
* Contents of a file outside the snapshot that a recent result named as
|
|
14256
|
+
* `external`, read from disk; null for every other path. This is the only
|
|
14257
|
+
* read done on a client's behalf, so a path the language server never
|
|
14258
|
+
* returned stays behind the snapshot allowlist.
|
|
14259
|
+
*/
|
|
14260
|
+
async readExternal(path) {
|
|
14261
|
+
if (!this.external.has(path)) return null;
|
|
14262
|
+
return readFile(path).catch(() => null);
|
|
14083
14263
|
}
|
|
14084
14264
|
async locate(uri) {
|
|
14085
14265
|
const path = await this.pathOf(uri);
|
|
14086
14266
|
if (path != null) {
|
|
14087
|
-
const text = this.
|
|
14267
|
+
const text = this.opts.textOf(path) ?? await this.opts.read(path);
|
|
14088
14268
|
if (text != null) return {
|
|
14089
14269
|
path,
|
|
14090
14270
|
text,
|
|
@@ -14121,11 +14301,6 @@ function relativeInside(root, abs) {
|
|
|
14121
14301
|
if (!rel || rel.startsWith("..") || isAbsolute(rel)) return null;
|
|
14122
14302
|
return rel.split("\\").join("/");
|
|
14123
14303
|
}
|
|
14124
|
-
/** How status messages name a server: `basedpyright-langserver --stdio` → `basedpyright-langserver`. */
|
|
14125
|
-
function programName(command) {
|
|
14126
|
-
const prog = argv0(command);
|
|
14127
|
-
return prog.split(/[\\/]/).pop() || prog;
|
|
14128
|
-
}
|
|
14129
14304
|
/**
|
|
14130
14305
|
* Canonical `p`, or `p` itself when it cannot be resolved. Uses libuv's
|
|
14131
14306
|
* realpath, the same one the async `realpath` in `pathOf` uses: the JS
|
|
@@ -14139,28 +14314,6 @@ function safeRealpathSync(p) {
|
|
|
14139
14314
|
return p;
|
|
14140
14315
|
}
|
|
14141
14316
|
}
|
|
14142
|
-
function toLsp(pos) {
|
|
14143
|
-
return {
|
|
14144
|
-
line: Math.max(0, pos.line - 1),
|
|
14145
|
-
character: Math.max(0, pos.col)
|
|
14146
|
-
};
|
|
14147
|
-
}
|
|
14148
|
-
function normalizeLocations(r) {
|
|
14149
|
-
if (!r) return [];
|
|
14150
|
-
return (Array.isArray(r) ? r : [r]).map((x) => "targetUri" in x ? {
|
|
14151
|
-
uri: x.targetUri,
|
|
14152
|
-
range: x.targetSelectionRange ?? x.targetRange
|
|
14153
|
-
} : x);
|
|
14154
|
-
}
|
|
14155
|
-
function isDocumentSymbols(r) {
|
|
14156
|
-
return r.length > 0 && !("location" in r[0]);
|
|
14157
|
-
}
|
|
14158
|
-
/**
|
|
14159
|
-
* `shell: true` runs the command under `sh -c`, so a plain kill would stop the
|
|
14160
|
-
* shell and orphan the language server. On POSIX the child leads its own process
|
|
14161
|
-
* group (`detached`), and shutdown signals the whole group.
|
|
14162
|
-
*/
|
|
14163
|
-
const DETACH = process.platform !== "win32";
|
|
14164
14317
|
function lineAt(text, line) {
|
|
14165
14318
|
let start = 0;
|
|
14166
14319
|
for (let i = 1; i < line; i++) {
|
|
@@ -14171,17 +14324,338 @@ function lineAt(text, line) {
|
|
|
14171
14324
|
const end = text.indexOf("\n", start);
|
|
14172
14325
|
return (end === -1 ? text.slice(start) : text.slice(start, end)).replace(/\r$/, "");
|
|
14173
14326
|
}
|
|
14174
|
-
|
|
14175
|
-
|
|
14176
|
-
|
|
14327
|
+
//#endregion
|
|
14328
|
+
//#region src/server/lsp/LspBridge.ts
|
|
14329
|
+
/** LSP `ContentModified`: the document moved under the request, so the answer would be stale. */
|
|
14330
|
+
const CONTENT_MODIFIED = -32801;
|
|
14331
|
+
/** Waits before re-asking a request the server refused as stale, one per attempt. */
|
|
14332
|
+
const CONTENT_MODIFIED_BACKOFF_MS = [40, 120];
|
|
14333
|
+
/**
|
|
14334
|
+
* One language server as diffle asks it: each LSP request against a document synced
|
|
14335
|
+
* from the snapshot, with the answer mapped back to snapshot paths. The process, the
|
|
14336
|
+
* open documents and the path mapping are `LspProcess`, `Documents` and `Locator`;
|
|
14337
|
+
* this is where the requests and their result shapes live.
|
|
14338
|
+
*/
|
|
14339
|
+
var LspBridge = class {
|
|
14340
|
+
opts;
|
|
14341
|
+
process;
|
|
14342
|
+
documents;
|
|
14343
|
+
locator;
|
|
14344
|
+
constructor(opts) {
|
|
14345
|
+
this.opts = opts;
|
|
14346
|
+
this.process = new LspProcess(opts);
|
|
14347
|
+
this.documents = new Documents({
|
|
14348
|
+
connection: () => this.process.connection(),
|
|
14349
|
+
read: opts.read,
|
|
14350
|
+
uri: (path) => this.uri(path),
|
|
14351
|
+
maxOpen: opts.maxOpen
|
|
14352
|
+
});
|
|
14353
|
+
this.locator = new Locator({
|
|
14354
|
+
root: opts.root,
|
|
14355
|
+
read: opts.read,
|
|
14356
|
+
has: opts.has,
|
|
14357
|
+
textOf: (path) => this.documents.textOf(path)
|
|
14358
|
+
});
|
|
14359
|
+
}
|
|
14360
|
+
status() {
|
|
14361
|
+
return this.process.status();
|
|
14362
|
+
}
|
|
14363
|
+
/** The retained log lines, oldest first. */
|
|
14364
|
+
entries() {
|
|
14365
|
+
return this.process.entries();
|
|
14366
|
+
}
|
|
14367
|
+
/** What the server's `initialize` result advertised; empty while starting. */
|
|
14368
|
+
features() {
|
|
14369
|
+
return this.process.features();
|
|
14370
|
+
}
|
|
14371
|
+
async definition(pos) {
|
|
14372
|
+
return this.locations("definition", "textDocument/definition", pos);
|
|
14373
|
+
}
|
|
14374
|
+
async typeDefinition(pos) {
|
|
14375
|
+
return this.locations("typeDefinition", "textDocument/typeDefinition", pos);
|
|
14376
|
+
}
|
|
14377
|
+
async references(pos) {
|
|
14378
|
+
return this.locations("references", "textDocument/references", pos, { context: { includeDeclaration: true } });
|
|
14379
|
+
}
|
|
14380
|
+
async hover(pos) {
|
|
14381
|
+
const { result, busy } = await this.query("textDocument/hover", {
|
|
14382
|
+
textDocument: { uri: this.uri(pos.path) },
|
|
14383
|
+
position: toLsp(pos)
|
|
14384
|
+
}, pos.path, "hover");
|
|
14385
|
+
const md = result ? hoverMarkdown(result.contents) : null;
|
|
14386
|
+
if (md == null) return inconclusive({ contents: null }, true, busy);
|
|
14387
|
+
const res = { contents: localizeFileLinks(md, await this.locator.links(md)) };
|
|
14388
|
+
if (result?.range) res.range = fromLsp(result.range);
|
|
14389
|
+
return res;
|
|
14390
|
+
}
|
|
14391
|
+
/**
|
|
14392
|
+
* Where else the symbol at `pos` occurs in its file, via `textDocument/documentHighlight`,
|
|
14393
|
+
* so scope and shadowing are the server's call. A server not offering the request, or a
|
|
14394
|
+
* position holding no symbol, yields no occurrences: the answer is advisory.
|
|
14395
|
+
*/
|
|
14396
|
+
async occurrences(pos) {
|
|
14397
|
+
const { result } = await this.query("textDocument/documentHighlight", {
|
|
14398
|
+
textDocument: { uri: this.uri(pos.path) },
|
|
14399
|
+
position: toLsp(pos)
|
|
14400
|
+
}, pos.path, "documentHighlight").catch(unsupported(null));
|
|
14401
|
+
return { occurrences: (result ?? []).map(({ range, kind }) => ({
|
|
14402
|
+
...fromLsp(range),
|
|
14403
|
+
kind: kind === 3 ? "write" : kind === 2 ? "read" : "text"
|
|
14404
|
+
})) };
|
|
14405
|
+
}
|
|
14406
|
+
/** Keeps the diff's own files open in the server (see `Documents.track`). Never throws. */
|
|
14407
|
+
track(paths) {
|
|
14408
|
+
return this.documents.track(paths);
|
|
14409
|
+
}
|
|
14410
|
+
async documentSymbols(path) {
|
|
14411
|
+
const { result, busy } = await this.query("textDocument/documentSymbol", { textDocument: { uri: this.uri(path) } }, path, "documentSymbol");
|
|
14412
|
+
const symbols = result ? flattenSymbols(result, path) : [];
|
|
14413
|
+
return inconclusive({ symbols }, !symbols.length, busy);
|
|
14414
|
+
}
|
|
14415
|
+
/**
|
|
14416
|
+
* Symbols the server knows for `query`, limited to paths the snapshot exposes, in the
|
|
14417
|
+
* server's order: the pool ranks the union. Membership comes from `has`, never from
|
|
14418
|
+
* reading the file: a per-keystroke search must not read every hit's text.
|
|
14419
|
+
*/
|
|
14420
|
+
async workspaceSymbols(query) {
|
|
14421
|
+
const { result, busy } = await this.query("workspace/symbol", { query }, void 0, "workspaceSymbol");
|
|
14422
|
+
if (!result) return inconclusive({ symbols: [] }, true, busy);
|
|
14423
|
+
const paths = await Promise.all(result.map((s) => this.locator.pathOf(s.location.uri)));
|
|
14424
|
+
const inside = await this.locator.membership(paths);
|
|
14425
|
+
const out = [];
|
|
14426
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14427
|
+
result.forEach((s, i) => {
|
|
14428
|
+
const path = paths[i];
|
|
14429
|
+
if (path == null || !inside.get(path)) return;
|
|
14430
|
+
const symbol = symbolInformation(s, path);
|
|
14431
|
+
const key = `${path}:${symbol.line}:${symbol.col}:${symbol.name}`;
|
|
14432
|
+
if (seen.has(key)) return;
|
|
14433
|
+
seen.add(key);
|
|
14434
|
+
out.push(symbol);
|
|
14435
|
+
});
|
|
14436
|
+
return inconclusive({ symbols: out }, !out.length, busy);
|
|
14437
|
+
}
|
|
14438
|
+
/** Reads a file outside the snapshot only if a result named it (see `Locator.readExternal`). */
|
|
14439
|
+
readExternal(path) {
|
|
14440
|
+
return this.locator.readExternal(path);
|
|
14441
|
+
}
|
|
14442
|
+
close() {
|
|
14443
|
+
return this.process.close();
|
|
14444
|
+
}
|
|
14445
|
+
async locations(feature, method, pos, extra = {}) {
|
|
14446
|
+
const { result, busy } = await this.query(method, {
|
|
14447
|
+
textDocument: { uri: this.uri(pos.path) },
|
|
14448
|
+
position: toLsp(pos),
|
|
14449
|
+
...extra
|
|
14450
|
+
}, pos.path, feature);
|
|
14451
|
+
const locations = await this.locator.locations(normalizeLocations(result));
|
|
14452
|
+
return inconclusive({ locations }, !locations.length, busy);
|
|
14453
|
+
}
|
|
14454
|
+
/**
|
|
14455
|
+
* One request against a synced document, re-asked while the server answers
|
|
14456
|
+
* `ContentModified`: something moved the document under it — a watcher refresh, or a
|
|
14457
|
+
* `track` pass opening the diff — so it refuses to answer from text it no longer holds.
|
|
14458
|
+
* Re-syncing and asking again is what an editor does. Giving up reads as an outage, the
|
|
14459
|
+
* one thing the reader can act on, rather than as a failed route. A `feature` the
|
|
14460
|
+
* server never advertised is not asked for. `busy` is why an empty `result` may be
|
|
14461
|
+
* incomplete: background work the server reported before or during the request.
|
|
14462
|
+
*/
|
|
14463
|
+
async query(method, params, path, feature) {
|
|
14464
|
+
for (let attempt = 0;; attempt++) {
|
|
14465
|
+
if (path != null) await this.documents.sync(path);
|
|
14466
|
+
const rpc = await this.process.connection();
|
|
14467
|
+
if (feature && !this.process.supports(feature)) throw new LspUnsupportedError(`${this.process.name} offers no ${LSP_FEATURE_LABELS[feature]}`);
|
|
14468
|
+
try {
|
|
14469
|
+
const pending = this.process.busy();
|
|
14470
|
+
return {
|
|
14471
|
+
result: await rpc.request(method, params),
|
|
14472
|
+
busy: this.process.busy() ?? pending
|
|
14473
|
+
};
|
|
14474
|
+
} catch (e) {
|
|
14475
|
+
if (!(e instanceof JsonRpcError) || e.code !== CONTENT_MODIFIED) throw e;
|
|
14476
|
+
const backoff = CONTENT_MODIFIED_BACKOFF_MS[attempt];
|
|
14477
|
+
if (backoff == null) throw new LspUnavailableError(`${this.process.name} could not answer while ${path ?? "the repository"} kept changing; try again`);
|
|
14478
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
14479
|
+
}
|
|
14480
|
+
}
|
|
14481
|
+
}
|
|
14482
|
+
uri(path) {
|
|
14483
|
+
return pathToFileURL(join(this.opts.root, path)).href;
|
|
14484
|
+
}
|
|
14485
|
+
};
|
|
14486
|
+
/** A catch handler answering `result` for a request the server does not offer; anything else propagates. */
|
|
14487
|
+
function unsupported(result) {
|
|
14488
|
+
return (e) => {
|
|
14489
|
+
if (e instanceof LspUnsupportedError) return {
|
|
14490
|
+
result,
|
|
14491
|
+
busy: null
|
|
14492
|
+
};
|
|
14493
|
+
throw e;
|
|
14494
|
+
};
|
|
14495
|
+
}
|
|
14496
|
+
//#endregion
|
|
14497
|
+
//#region src/server/lsp/merge.ts
|
|
14498
|
+
/**
|
|
14499
|
+
* The union of what several servers answered. A server that fails drops its share;
|
|
14500
|
+
* every one failing is the first failure. When the union is `empty`, the first reason it
|
|
14501
|
+
* may be incomplete stands: a server's own, else a failed server's message, so an
|
|
14502
|
+
* owner still starting reads as such rather than as silence.
|
|
14503
|
+
*/
|
|
14504
|
+
async function union(asked, merge, empty) {
|
|
14505
|
+
const results = await Promise.allSettled(asked);
|
|
14506
|
+
const answers = results.flatMap((r) => r.status === "fulfilled" ? [r.value] : []);
|
|
14507
|
+
const failed = results.find((r) => r.status === "rejected");
|
|
14508
|
+
if (!answers.length && failed) throw failed.reason;
|
|
14509
|
+
const merged = merge(answers);
|
|
14510
|
+
if (empty(merged)) {
|
|
14511
|
+
const reason = answers.find((a) => a.inconclusive)?.inconclusive ?? (failed ? messageOf(failed.reason) : void 0);
|
|
14512
|
+
if (reason) merged.inconclusive = reason;
|
|
14513
|
+
}
|
|
14514
|
+
return merged;
|
|
14515
|
+
}
|
|
14516
|
+
function messageOf(e) {
|
|
14517
|
+
return e instanceof Error ? e.message : String(e);
|
|
14518
|
+
}
|
|
14519
|
+
/** Every server's hover, owner first, as one tooltip; the span is the first one offered. */
|
|
14520
|
+
function mergeHovers(answers) {
|
|
14521
|
+
const said = answers.filter((a) => a.contents != null);
|
|
14522
|
+
if (!said.length) return { contents: null };
|
|
14523
|
+
const range = said.find((a) => a.range)?.range;
|
|
14524
|
+
return {
|
|
14525
|
+
contents: said.map((a) => a.contents).join("\n\n---\n\n"),
|
|
14526
|
+
...range ? { range } : {}
|
|
14527
|
+
};
|
|
14528
|
+
}
|
|
14529
|
+
/** The union of every server's locations, owner first, each place once. */
|
|
14530
|
+
function mergeLocations(answers) {
|
|
14531
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14532
|
+
return { locations: answers.flatMap((a) => a.locations.filter((l) => {
|
|
14533
|
+
const key = `${l.path}\0${l.line}\0${l.col}`;
|
|
14534
|
+
if (seen.has(key)) return false;
|
|
14535
|
+
seen.add(key);
|
|
14536
|
+
return true;
|
|
14537
|
+
})) };
|
|
14538
|
+
}
|
|
14539
|
+
//#endregion
|
|
14540
|
+
//#region src/server/lsp/tsdk.ts
|
|
14541
|
+
/**
|
|
14542
|
+
* The TypeScript `lib` directory a Volar server such as astro-ls loads as its
|
|
14543
|
+
* `typescript.tsdk`, or null when none is found. The repository's own `typescript`
|
|
14544
|
+
* wins, so the server reads code with the project's version; after it, the one
|
|
14545
|
+
* installed beside the first of `executables` that has one. Node's resolution from an
|
|
14546
|
+
* executable's real location reaches the package in the layouts of npm (hoisted under
|
|
14547
|
+
* `lib/node_modules`), a Windows `.cmd` shim (beside `node_modules`) and nixpkgs (the
|
|
14548
|
+
* pnpm store the symlink's target sits in). A conda or pixi environment runs a launcher
|
|
14549
|
+
* in `bin`, so each package under its `lib/node_modules` is tried as well, where the
|
|
14550
|
+
* TypeScript is nested or hoisted. TypeScript 7 ships no `typescript.js`, so it is
|
|
14551
|
+
* passed over: Volar cannot load the Go port.
|
|
14552
|
+
*/
|
|
14553
|
+
function tsdkLocation(root, executables) {
|
|
14554
|
+
const bases = [root];
|
|
14555
|
+
for (const executable of executables) {
|
|
14556
|
+
if (executable == null) continue;
|
|
14557
|
+
const bin = dirname(realExecutable(executable));
|
|
14558
|
+
bases.push(bin, ...globalPackages(resolve(bin, "..", "lib", "node_modules")));
|
|
14559
|
+
}
|
|
14560
|
+
for (const base of bases) {
|
|
14561
|
+
const lib = typescriptLib(base);
|
|
14562
|
+
if (lib) return lib;
|
|
14563
|
+
}
|
|
14564
|
+
return null;
|
|
14565
|
+
}
|
|
14566
|
+
/** The package directories under a global `node_modules`, scoped ones included; none when it is not there. */
|
|
14567
|
+
function globalPackages(modules) {
|
|
14568
|
+
const entries = (dir) => {
|
|
14569
|
+
try {
|
|
14570
|
+
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
|
|
14571
|
+
} catch {
|
|
14572
|
+
return [];
|
|
14573
|
+
}
|
|
14574
|
+
};
|
|
14575
|
+
return entries(modules).flatMap((name) => name.startsWith("@") ? entries(join(modules, name)).map((sub) => join(modules, name, sub)) : [join(modules, name)]);
|
|
14576
|
+
}
|
|
14577
|
+
/** `typescript/lib` as resolved from `base`, when it holds a loadable TypeScript. */
|
|
14578
|
+
function typescriptLib(base) {
|
|
14579
|
+
try {
|
|
14580
|
+
const lib = join(dirname(createRequire(join(base, "package.json")).resolve("typescript/package.json")), "lib");
|
|
14581
|
+
return ["typescript.js", "tsserverlibrary.js"].some((file) => existsSync(join(lib, file))) ? lib : null;
|
|
14582
|
+
} catch {
|
|
14583
|
+
return null;
|
|
14584
|
+
}
|
|
14585
|
+
}
|
|
14586
|
+
//#endregion
|
|
14587
|
+
//#region src/server/lsp/vue.ts
|
|
14588
|
+
/** The tsserver plugin that turns .vue files into TypeScript documents. */
|
|
14589
|
+
const VUE_PLUGIN = "@vue/typescript-plugin";
|
|
14590
|
+
/**
|
|
14591
|
+
* Where tsserver loads the Vue plugin from, given the `vue-language-server` executable,
|
|
14592
|
+
* or null when no package holding the plugin is found beside it. Vue language tools 3
|
|
14593
|
+
* serve .vue files through this plugin inside tsserver; the standalone server only
|
|
14594
|
+
* relays to a tsserver the editor must bridge, so diffle never runs it. The package is
|
|
14595
|
+
* reached through the layouts of npm (a symlink into the package, or a `.cmd` shim
|
|
14596
|
+
* beside `node_modules` on Windows), conda and pixi (a launcher in `bin` beside
|
|
14597
|
+
* `lib/node_modules`, behind a pixi global trampoline), and nixpkgs (a wrapper beside
|
|
14598
|
+
* `lib/language-tools`).
|
|
14599
|
+
*/
|
|
14600
|
+
function vuePluginLocation(executable) {
|
|
14601
|
+
const real = realExecutable(executable);
|
|
14602
|
+
const candidates = [
|
|
14603
|
+
resolve(dirname(real), ".."),
|
|
14604
|
+
join(dirname(executable), "node_modules", "@vue", "language-server"),
|
|
14605
|
+
resolve(dirname(real), "..", "lib", "node_modules", "@vue", "language-server"),
|
|
14606
|
+
resolve(dirname(real), "..", "lib", "language-tools", "packages", "language-server")
|
|
14607
|
+
];
|
|
14608
|
+
for (const dir of candidates) try {
|
|
14609
|
+
createRequire(join(dir, "package.json")).resolve(VUE_PLUGIN);
|
|
14610
|
+
return dir;
|
|
14611
|
+
} catch {}
|
|
14612
|
+
return null;
|
|
14177
14613
|
}
|
|
14178
|
-
/**
|
|
14179
|
-
|
|
14180
|
-
|
|
14181
|
-
|
|
14614
|
+
/**
|
|
14615
|
+
* The tsserver plugin entry for .vue files when `vue-language-server` is on PATH, else
|
|
14616
|
+
* null. Loaded whenever a TypeScript server starts, not only when the diff holds a .vue
|
|
14617
|
+
* file: a process serving .ts files first and .vue files after a refresh cannot load it
|
|
14618
|
+
* later, and .ts files importing components resolve them. `lookup` is the PATH probe.
|
|
14619
|
+
*/
|
|
14620
|
+
function vuePlugin(lookup) {
|
|
14621
|
+
const executable = lookup("vue-language-server");
|
|
14622
|
+
const location = executable == null ? null : vuePluginLocation(executable);
|
|
14623
|
+
return location == null ? null : {
|
|
14624
|
+
name: VUE_PLUGIN,
|
|
14625
|
+
location,
|
|
14626
|
+
languages: ["vue"]
|
|
14627
|
+
};
|
|
14182
14628
|
}
|
|
14183
14629
|
//#endregion
|
|
14184
14630
|
//#region src/server/lsp/registry.ts
|
|
14631
|
+
const TYPESCRIPT = [
|
|
14632
|
+
"typescript",
|
|
14633
|
+
"typescriptreact",
|
|
14634
|
+
"javascript",
|
|
14635
|
+
"javascriptreact"
|
|
14636
|
+
];
|
|
14637
|
+
const TSSERVER = {
|
|
14638
|
+
command: "typescript-language-server --stdio",
|
|
14639
|
+
initializationOptions: ({ lookup }) => {
|
|
14640
|
+
const plugin = vuePlugin(lookup);
|
|
14641
|
+
return plugin ? { plugins: [plugin] } : void 0;
|
|
14642
|
+
}
|
|
14643
|
+
};
|
|
14644
|
+
const VTSLS = {
|
|
14645
|
+
command: "vtsls --stdio",
|
|
14646
|
+
settings: ({ lookup }) => {
|
|
14647
|
+
const plugin = vuePlugin(lookup);
|
|
14648
|
+
return plugin ? { vtsls: { tsserver: { globalPlugins: [{
|
|
14649
|
+
...plugin,
|
|
14650
|
+
configNamespace: "typescript"
|
|
14651
|
+
}] } } } : {};
|
|
14652
|
+
}
|
|
14653
|
+
};
|
|
14654
|
+
const VUE = {
|
|
14655
|
+
program: "vue-language-server",
|
|
14656
|
+
what: VUE_PLUGIN,
|
|
14657
|
+
locate: vuePluginLocation
|
|
14658
|
+
};
|
|
14185
14659
|
/**
|
|
14186
14660
|
* Built-in servers, in preference order per language: the first one whose program is
|
|
14187
14661
|
* on PATH is used. Entries serving several languages become one process, which is how
|
|
@@ -14213,22 +14687,34 @@ const CANDIDATES = [
|
|
|
14213
14687
|
command: "jedi-language-server"
|
|
14214
14688
|
},
|
|
14215
14689
|
{
|
|
14216
|
-
languages:
|
|
14217
|
-
|
|
14218
|
-
"typescriptreact",
|
|
14219
|
-
"javascript",
|
|
14220
|
-
"javascriptreact"
|
|
14221
|
-
],
|
|
14222
|
-
command: "typescript-language-server --stdio"
|
|
14690
|
+
languages: TYPESCRIPT,
|
|
14691
|
+
...TSSERVER
|
|
14223
14692
|
},
|
|
14224
14693
|
{
|
|
14225
|
-
languages:
|
|
14226
|
-
|
|
14227
|
-
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
|
|
14231
|
-
|
|
14694
|
+
languages: TYPESCRIPT,
|
|
14695
|
+
...VTSLS
|
|
14696
|
+
},
|
|
14697
|
+
{
|
|
14698
|
+
languages: ["vue"],
|
|
14699
|
+
...TSSERVER,
|
|
14700
|
+
requires: VUE
|
|
14701
|
+
},
|
|
14702
|
+
{
|
|
14703
|
+
languages: ["vue"],
|
|
14704
|
+
...VTSLS,
|
|
14705
|
+
requires: VUE
|
|
14706
|
+
},
|
|
14707
|
+
{
|
|
14708
|
+
languages: ["astro"],
|
|
14709
|
+
command: "astro-ls --stdio",
|
|
14710
|
+
initializationOptions: ({ root, lookup }) => {
|
|
14711
|
+
const tsdk = tsdkLocation(root, [
|
|
14712
|
+
"astro-ls",
|
|
14713
|
+
"typescript-language-server",
|
|
14714
|
+
"vtsls"
|
|
14715
|
+
].map((c) => lookup(c)));
|
|
14716
|
+
return tsdk ? { typescript: { tsdk } } : void 0;
|
|
14717
|
+
}
|
|
14232
14718
|
},
|
|
14233
14719
|
{
|
|
14234
14720
|
languages: ["rust"],
|
|
@@ -14254,6 +14740,18 @@ const CANDIDATES = [
|
|
|
14254
14740
|
languages: ["java"],
|
|
14255
14741
|
command: "jdtls"
|
|
14256
14742
|
},
|
|
14743
|
+
{
|
|
14744
|
+
languages: ["csharp"],
|
|
14745
|
+
command: "roslyn-language-server --stdio --autoLoadProjects"
|
|
14746
|
+
},
|
|
14747
|
+
{
|
|
14748
|
+
languages: ["csharp"],
|
|
14749
|
+
command: "Microsoft.CodeAnalysis.LanguageServer --stdio --autoLoadProjects"
|
|
14750
|
+
},
|
|
14751
|
+
{
|
|
14752
|
+
languages: ["csharp"],
|
|
14753
|
+
command: "csharp-ls"
|
|
14754
|
+
},
|
|
14257
14755
|
{
|
|
14258
14756
|
languages: ["lua"],
|
|
14259
14757
|
command: "lua-language-server"
|
|
@@ -14300,15 +14798,24 @@ const CANDIDATES = [
|
|
|
14300
14798
|
},
|
|
14301
14799
|
{
|
|
14302
14800
|
languages: ["json", "jsonc"],
|
|
14303
|
-
command: "vscode-json-language-server --stdio"
|
|
14801
|
+
command: "vscode-json-language-server --stdio",
|
|
14802
|
+
settings: async () => ({ json: { schemas: await loadSchemaCatalog() } })
|
|
14304
14803
|
},
|
|
14305
14804
|
{
|
|
14306
14805
|
languages: ["json", "jsonc"],
|
|
14307
|
-
command: "vscode-json-languageserver --stdio"
|
|
14806
|
+
command: "vscode-json-languageserver --stdio",
|
|
14807
|
+
settings: async () => ({ json: { schemas: await loadSchemaCatalog() } })
|
|
14308
14808
|
},
|
|
14309
14809
|
{
|
|
14310
14810
|
languages: ["yaml"],
|
|
14311
|
-
command: "yaml-language-server --stdio"
|
|
14811
|
+
command: "yaml-language-server --stdio",
|
|
14812
|
+
settings: () => ({ yaml: {
|
|
14813
|
+
hover: true,
|
|
14814
|
+
schemaStore: {
|
|
14815
|
+
enable: true,
|
|
14816
|
+
url: SCHEMA_CATALOG
|
|
14817
|
+
}
|
|
14818
|
+
} })
|
|
14312
14819
|
},
|
|
14313
14820
|
{
|
|
14314
14821
|
languages: ["toml"],
|
|
@@ -14316,35 +14823,117 @@ const CANDIDATES = [
|
|
|
14316
14823
|
},
|
|
14317
14824
|
{
|
|
14318
14825
|
languages: ["toml"],
|
|
14319
|
-
command: "taplo lsp stdio"
|
|
14826
|
+
command: "taplo lsp stdio",
|
|
14827
|
+
settings: () => ({ evenBetterToml: { schema: {
|
|
14828
|
+
enabled: true,
|
|
14829
|
+
catalogs: [SCHEMA_CATALOG]
|
|
14830
|
+
} } })
|
|
14831
|
+
},
|
|
14832
|
+
{
|
|
14833
|
+
languages: ["css"],
|
|
14834
|
+
command: "vscode-css-language-server --stdio",
|
|
14835
|
+
settings: () => ({
|
|
14836
|
+
css: { validate: false },
|
|
14837
|
+
less: { validate: false },
|
|
14838
|
+
scss: { validate: false }
|
|
14839
|
+
})
|
|
14840
|
+
},
|
|
14841
|
+
{
|
|
14842
|
+
languages: ["html"],
|
|
14843
|
+
command: "vscode-html-language-server --stdio"
|
|
14844
|
+
},
|
|
14845
|
+
{
|
|
14846
|
+
languages: [],
|
|
14847
|
+
joins: [
|
|
14848
|
+
"astro",
|
|
14849
|
+
"css",
|
|
14850
|
+
"html",
|
|
14851
|
+
"javascript",
|
|
14852
|
+
"javascriptreact",
|
|
14853
|
+
"typescript",
|
|
14854
|
+
"typescriptreact",
|
|
14855
|
+
"vue"
|
|
14856
|
+
],
|
|
14857
|
+
command: "tailwindcss-language-server --stdio"
|
|
14320
14858
|
}
|
|
14321
14859
|
];
|
|
14322
|
-
/**
|
|
14323
|
-
|
|
14324
|
-
|
|
14860
|
+
/**
|
|
14861
|
+
* How a command line's program is configured, by the built-in definition for that program:
|
|
14862
|
+
* an override naming a known server with other flags or a full path still gets its
|
|
14863
|
+
* settings. A program diffle does not know takes none.
|
|
14864
|
+
*/
|
|
14865
|
+
function definitionOf(command) {
|
|
14866
|
+
const program = programOf(command);
|
|
14867
|
+
const known = CANDIDATES.find((c) => programOf(c.command) === program);
|
|
14868
|
+
return {
|
|
14869
|
+
command,
|
|
14870
|
+
settings: known?.settings,
|
|
14871
|
+
initializationOptions: known?.initializationOptions
|
|
14872
|
+
};
|
|
14325
14873
|
}
|
|
14326
14874
|
/**
|
|
14327
14875
|
* Which servers to start for `languages`. A config override replaces the candidate
|
|
14328
14876
|
* list for its language and is never probed: the user named a command, so a typo
|
|
14329
14877
|
* should surface as the server failing to start, not as silence. An override of `''`
|
|
14330
14878
|
* turns the language off. Languages resolving to the same command line share one
|
|
14331
|
-
* process. `
|
|
14879
|
+
* process. A missing language's `tried` names the programs that were not on PATH, so
|
|
14880
|
+
* a candidate needing two programs lists only the absent one; a program on PATH that
|
|
14881
|
+
* lacks what the candidate loads from beside it is as good as absent, and the first
|
|
14882
|
+
* such finding is the language's `why`. A candidate joining a requested language
|
|
14883
|
+
* starts too, when on PATH, whatever owns it, and joins only the requested ones,
|
|
14884
|
+
* growing like an owner does as later snapshots ask for more; its absence is no one's
|
|
14885
|
+
* missing server. `lookup` is a test seam for the PATH probe.
|
|
14332
14886
|
*/
|
|
14333
14887
|
function resolveServers(languages, overrides = {}, lookup = (c) => findOnPath(c)) {
|
|
14334
|
-
const
|
|
14335
|
-
const
|
|
14336
|
-
let hit =
|
|
14337
|
-
if (hit
|
|
14888
|
+
const probed = /* @__PURE__ */ new Map();
|
|
14889
|
+
const where = (command) => {
|
|
14890
|
+
let hit = probed.get(command);
|
|
14891
|
+
if (hit === void 0) probed.set(command, hit = lookup(command));
|
|
14338
14892
|
return hit;
|
|
14339
14893
|
};
|
|
14894
|
+
/** What keeps a candidate from starting: programs not on PATH, or one on PATH without what it loads from beside it. */
|
|
14895
|
+
const probe = (candidate) => {
|
|
14896
|
+
const absent = [];
|
|
14897
|
+
if (where(candidate.command) == null) absent.push(argv0(candidate.command));
|
|
14898
|
+
const { requires } = candidate;
|
|
14899
|
+
if (!requires) return { absent };
|
|
14900
|
+
const executable = where(requires.program);
|
|
14901
|
+
if (executable == null) absent.push(requires.program);
|
|
14902
|
+
else if (requires.locate(executable) == null) return {
|
|
14903
|
+
absent,
|
|
14904
|
+
why: `${requires.program} is on PATH but ${requires.what} was not found beside it`
|
|
14905
|
+
};
|
|
14906
|
+
return { absent };
|
|
14907
|
+
};
|
|
14908
|
+
const startable = (candidate) => {
|
|
14909
|
+
const { absent, why } = probe(candidate);
|
|
14910
|
+
return !absent.length && !why;
|
|
14911
|
+
};
|
|
14340
14912
|
const servers = /* @__PURE__ */ new Map();
|
|
14341
14913
|
const missing = [];
|
|
14342
|
-
|
|
14343
|
-
|
|
14914
|
+
const asked = [...new Set(languages)].sort();
|
|
14915
|
+
for (const language of asked) {
|
|
14916
|
+
let command = overrides[language]?.trim();
|
|
14917
|
+
const tried = [];
|
|
14918
|
+
let why;
|
|
14919
|
+
if (command == null) for (const candidate of CANDIDATES) {
|
|
14920
|
+
if (!candidate.languages.includes(language)) continue;
|
|
14921
|
+
const { absent, why: broken } = probe(candidate);
|
|
14922
|
+
if (!absent.length && !broken) {
|
|
14923
|
+
command = candidate.command;
|
|
14924
|
+
break;
|
|
14925
|
+
}
|
|
14926
|
+
why ??= broken;
|
|
14927
|
+
for (const program of absent) if (!tried.includes(program)) tried.push(program);
|
|
14928
|
+
}
|
|
14344
14929
|
if (command == null || command === "") {
|
|
14345
|
-
missing.push({
|
|
14930
|
+
missing.push(why ? {
|
|
14346
14931
|
language,
|
|
14347
|
-
tried
|
|
14932
|
+
tried,
|
|
14933
|
+
why
|
|
14934
|
+
} : {
|
|
14935
|
+
language,
|
|
14936
|
+
tried
|
|
14348
14937
|
});
|
|
14349
14938
|
continue;
|
|
14350
14939
|
}
|
|
@@ -14352,9 +14941,21 @@ function resolveServers(languages, overrides = {}, lookup = (c) => findOnPath(c)
|
|
|
14352
14941
|
if (server) server.languages.push(language);
|
|
14353
14942
|
else servers.set(command, {
|
|
14354
14943
|
command,
|
|
14355
|
-
languages: [language]
|
|
14944
|
+
languages: [language],
|
|
14945
|
+
joins: []
|
|
14356
14946
|
});
|
|
14357
14947
|
}
|
|
14948
|
+
for (const candidate of CANDIDATES) {
|
|
14949
|
+
const joins = (candidate.joins ?? []).filter((l) => asked.includes(l));
|
|
14950
|
+
if (!joins.length || !startable(candidate)) continue;
|
|
14951
|
+
const server = servers.get(candidate.command) ?? {
|
|
14952
|
+
command: candidate.command,
|
|
14953
|
+
languages: [],
|
|
14954
|
+
joins: []
|
|
14955
|
+
};
|
|
14956
|
+
servers.set(candidate.command, server);
|
|
14957
|
+
for (const l of joins) if (!server.languages.includes(l) && !server.joins.includes(l)) server.joins.push(l);
|
|
14958
|
+
}
|
|
14358
14959
|
return {
|
|
14359
14960
|
servers: [...servers.values()],
|
|
14360
14961
|
missing
|
|
@@ -14363,21 +14964,31 @@ function resolveServers(languages, overrides = {}, lookup = (c) => findOnPath(c)
|
|
|
14363
14964
|
//#endregion
|
|
14364
14965
|
//#region src/server/lsp/LspPool.ts
|
|
14365
14966
|
/**
|
|
14366
|
-
*
|
|
14367
|
-
*
|
|
14368
|
-
*
|
|
14369
|
-
|
|
14967
|
+
* A status change goes out at once when the pool has been quiet, and later ones within
|
|
14968
|
+
* this window ride on one trailing publish: a server logging a hundred stderr lines
|
|
14969
|
+
* while indexing is a few broadcasts, not a hundred.
|
|
14970
|
+
*/
|
|
14971
|
+
const PUBLISH_MS = 20;
|
|
14972
|
+
/**
|
|
14973
|
+
* The language servers of one run, started the first time a snapshot names a language
|
|
14974
|
+
* they serve and kept for the run. Routes like an editor: every server for a file's
|
|
14975
|
+
* language is asked at once and their hovers, definitions and references are merged,
|
|
14976
|
+
* owner first; highlights and outlines come from the owner alone, as two
|
|
14977
|
+
* servers' highlight sets would only clash. Repository-wide queries fan out to all
|
|
14978
|
+
* servers, and all of them report as one status. Owns no protocol logic: each server
|
|
14979
|
+
* is an `LspBridge`.
|
|
14370
14980
|
*/
|
|
14371
14981
|
var LspPool = class {
|
|
14372
14982
|
opts;
|
|
14373
14983
|
/** Servers by command line: languages that resolve to the same command share one process. */
|
|
14374
14984
|
entries = /* @__PURE__ */ new Map();
|
|
14375
|
-
byLanguage = /* @__PURE__ */ new Map();
|
|
14376
14985
|
missing = [];
|
|
14377
14986
|
/** Languages already resolved, served or not, so the PATH probe runs once each. */
|
|
14378
14987
|
resolved = /* @__PURE__ */ new Set();
|
|
14379
14988
|
closing = false;
|
|
14380
14989
|
closed;
|
|
14990
|
+
publishTimer = null;
|
|
14991
|
+
publishDue = false;
|
|
14381
14992
|
constructor(opts) {
|
|
14382
14993
|
this.opts = opts;
|
|
14383
14994
|
this.ensure(opts.preload ?? []);
|
|
@@ -14387,11 +14998,17 @@ var LspPool = class {
|
|
|
14387
14998
|
enabled: true,
|
|
14388
14999
|
servers: [...this.entries.values()].map((e) => ({
|
|
14389
15000
|
...e.bridge.status(),
|
|
14390
|
-
languages: [...e.languages]
|
|
15001
|
+
languages: [...e.languages],
|
|
15002
|
+
joins: [...e.joins],
|
|
15003
|
+
supports: e.bridge.features()
|
|
14391
15004
|
})),
|
|
14392
15005
|
missing: this.missing
|
|
14393
15006
|
};
|
|
14394
15007
|
}
|
|
15008
|
+
/** The retained log lines of the server started by `command`, or null when none was. */
|
|
15009
|
+
log(command) {
|
|
15010
|
+
return this.entries.get(command)?.bridge.entries() ?? null;
|
|
15011
|
+
}
|
|
14395
15012
|
/** Reads an external file only if a running server named it. */
|
|
14396
15013
|
async readExternal(path) {
|
|
14397
15014
|
for (const { bridge } of this.entries.values()) {
|
|
@@ -14400,47 +15017,44 @@ var LspPool = class {
|
|
|
14400
15017
|
}
|
|
14401
15018
|
return null;
|
|
14402
15019
|
}
|
|
14403
|
-
|
|
14404
|
-
return this.
|
|
14405
|
-
}
|
|
14406
|
-
async typeDefinition(pos) {
|
|
14407
|
-
return this.bridgeFor(pos.path).typeDefinition(pos);
|
|
15020
|
+
definition(pos) {
|
|
15021
|
+
return this.locations(pos, (b) => b.definition(pos));
|
|
14408
15022
|
}
|
|
14409
|
-
|
|
14410
|
-
return this.
|
|
15023
|
+
typeDefinition(pos) {
|
|
15024
|
+
return this.locations(pos, (b) => b.typeDefinition(pos));
|
|
14411
15025
|
}
|
|
14412
|
-
|
|
14413
|
-
return this.
|
|
15026
|
+
references(pos) {
|
|
15027
|
+
return this.locations(pos, (b) => b.references(pos));
|
|
14414
15028
|
}
|
|
14415
|
-
|
|
14416
|
-
return this.
|
|
15029
|
+
hover(pos) {
|
|
15030
|
+
return this.fanOut(pos.path, (b) => b.hover(pos), mergeHovers, (r) => r.contents == null);
|
|
14417
15031
|
}
|
|
14418
15032
|
async occurrences(pos) {
|
|
14419
|
-
return this.
|
|
15033
|
+
return this.ownerFor(pos.path).occurrences(pos);
|
|
14420
15034
|
}
|
|
14421
15035
|
async documentSymbols(path) {
|
|
14422
|
-
return this.
|
|
15036
|
+
return this.ownerFor(path).documentSymbols(path);
|
|
14423
15037
|
}
|
|
14424
15038
|
/**
|
|
14425
|
-
* Symbols matching `query` from every running server, best `fuzzyRank` match first.
|
|
14426
|
-
*
|
|
15039
|
+
* Symbols matching `query` from every running server, best `fuzzyRank` match first.
|
|
15040
|
+
* Ranked before the cap: servers list loose matches in their own order.
|
|
14427
15041
|
*/
|
|
14428
15042
|
async workspaceSymbols(query, limit = 200) {
|
|
15043
|
+
if (this.closing) throw new LspUnavailableError("the language servers are shutting down");
|
|
14429
15044
|
const bridges = [...this.entries.values()].map((e) => e.bridge);
|
|
14430
|
-
if (!bridges.length) throw new LspUnavailableError(
|
|
14431
|
-
|
|
14432
|
-
const out = results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
|
|
14433
|
-
const failed = results.find((r) => r.status === "rejected");
|
|
14434
|
-
if (!out.length && failed) throw failed.reason;
|
|
14435
|
-
return fuzzyRank(out, query, (s) => s.name).slice(0, limit);
|
|
15045
|
+
if (!bridges.length) throw new LspUnavailableError("no language server is running");
|
|
15046
|
+
return union(bridges.map((b) => b.workspaceSymbols(query)), (answers) => ({ symbols: fuzzyRank(answers.flatMap((a) => a.symbols), query, (s) => s.name).slice(0, limit) }), (r) => !r.symbols.length);
|
|
14436
15047
|
}
|
|
14437
15048
|
/**
|
|
14438
|
-
* Keeps the diff's files open in
|
|
15049
|
+
* Keeps the diff's files open in every server for their language, starting one for each
|
|
14439
15050
|
* language the diff has newly grown. A server whose language left the diff stays up
|
|
14440
|
-
* with nothing tracked, so its index is warm if the reader switches back.
|
|
15051
|
+
* with nothing tracked, so its index is warm if the reader switches back. Nothing is
|
|
15052
|
+
* tracked when the new side is not the checkout: a server would see text that
|
|
15053
|
+
* contradicts the disk it indexes, and `lspBlocker` refuses queries there anyway.
|
|
14441
15054
|
*/
|
|
14442
|
-
track(
|
|
15055
|
+
track(snap) {
|
|
14443
15056
|
if (this.closing) return Promise.resolve();
|
|
15057
|
+
const paths = followsCheckout(snap) ? snap.changed.filter((f) => f.status !== "D" && !f.binary).map((f) => f.path) : [];
|
|
14444
15058
|
const byLanguage = /* @__PURE__ */ new Map();
|
|
14445
15059
|
for (const path of paths) {
|
|
14446
15060
|
const language = languageOf(path);
|
|
@@ -14451,23 +15065,38 @@ var LspPool = class {
|
|
|
14451
15065
|
}
|
|
14452
15066
|
this.ensure(byLanguage.keys());
|
|
14453
15067
|
const entries = [...this.entries.values()];
|
|
14454
|
-
return Promise.all(entries.map((e) => e.bridge.track(e.languages.flatMap((l) => byLanguage.get(l) ?? [])))).then(() => {});
|
|
15068
|
+
return Promise.all(entries.map((e) => e.bridge.track([...e.languages, ...e.joins].flatMap((l) => byLanguage.get(l) ?? [])))).then(() => {});
|
|
14455
15069
|
}
|
|
14456
15070
|
/** Idempotent: the CLI's dispose can run beside a signal handler's. */
|
|
14457
15071
|
close() {
|
|
14458
15072
|
this.closing = true;
|
|
15073
|
+
if (this.publishTimer) clearTimeout(this.publishTimer);
|
|
15074
|
+
this.publishTimer = null;
|
|
14459
15075
|
return this.closed ??= Promise.all([...this.entries.values()].map((e) => e.bridge.close())).then(() => {});
|
|
14460
15076
|
}
|
|
14461
|
-
/**
|
|
14462
|
-
|
|
15077
|
+
/**
|
|
15078
|
+
* Every server for `path`, owner first. A language nothing owns is still served by the
|
|
15079
|
+
* servers joining it. The route has already asked `lspBlocker` why none would answer,
|
|
15080
|
+
* so an empty list here is a race with a snapshot or a shutdown, not a message to word.
|
|
15081
|
+
*/
|
|
15082
|
+
bridgesFor(path) {
|
|
14463
15083
|
if (this.closing) throw new LspUnavailableError("the language servers are shutting down");
|
|
14464
15084
|
const language = languageOf(path);
|
|
14465
|
-
const
|
|
14466
|
-
|
|
14467
|
-
|
|
15085
|
+
const entries = [...this.entries.values()];
|
|
15086
|
+
const bridges = language ? [...entries.filter((e) => e.languages.includes(language)), ...entries.filter((e) => e.joins.includes(language))].map((e) => e.bridge) : [];
|
|
15087
|
+
if (!bridges.length) throw new LspUnavailableError(`no language server for ${path}`);
|
|
15088
|
+
return bridges;
|
|
15089
|
+
}
|
|
15090
|
+
ownerFor(path) {
|
|
15091
|
+
return this.bridgesFor(path)[0];
|
|
15092
|
+
}
|
|
15093
|
+
/** Every server's locations for `pos`, owner first, each place once. */
|
|
15094
|
+
locations(pos, ask) {
|
|
15095
|
+
return this.fanOut(pos.path, ask, mergeLocations, (r) => !r.locations.length);
|
|
14468
15096
|
}
|
|
14469
|
-
|
|
14470
|
-
|
|
15097
|
+
/** Asks every server for `path` at once and merges what they answered (see `union`). */
|
|
15098
|
+
async fanOut(path, ask, merge, empty) {
|
|
15099
|
+
return union(this.bridgesFor(path).map(ask), merge, empty);
|
|
14471
15100
|
}
|
|
14472
15101
|
/** Resolves and starts servers for languages not seen before. Cheap and idempotent per language. */
|
|
14473
15102
|
ensure(languages) {
|
|
@@ -14478,19 +15107,24 @@ var LspPool = class {
|
|
|
14478
15107
|
const { servers, missing } = resolveServers(fresh, this.opts.overrides, this.opts.lookup);
|
|
14479
15108
|
this.missing.push(...missing);
|
|
14480
15109
|
for (const server of servers) {
|
|
14481
|
-
const entry = this.entries.get(server.command) ?? this.spawn(server.command
|
|
14482
|
-
for (const l of server.languages)
|
|
14483
|
-
|
|
14484
|
-
this.byLanguage.set(l, entry.bridge);
|
|
14485
|
-
}
|
|
15110
|
+
const entry = this.entries.get(server.command) ?? this.spawn(server.command);
|
|
15111
|
+
for (const l of server.languages) if (!entry.languages.includes(l)) entry.languages.push(l);
|
|
15112
|
+
for (const l of server.joins) if (!entry.joins.includes(l)) entry.joins.push(l);
|
|
14486
15113
|
}
|
|
14487
15114
|
if (servers.length || missing.length) this.publish();
|
|
14488
15115
|
}
|
|
14489
|
-
spawn(command
|
|
15116
|
+
spawn(command) {
|
|
15117
|
+
const definition = definitionOf(command);
|
|
15118
|
+
const ctx = {
|
|
15119
|
+
root: this.opts.root,
|
|
15120
|
+
lookup: this.opts.lookup ?? findOnPath
|
|
15121
|
+
};
|
|
14490
15122
|
const entry = {
|
|
14491
|
-
|
|
15123
|
+
command,
|
|
15124
|
+
bridge: new LspBridge({
|
|
14492
15125
|
command,
|
|
14493
|
-
settings:
|
|
15126
|
+
settings: definition.settings ? Promise.resolve(definition.settings(ctx)) : void 0,
|
|
15127
|
+
initializationOptions: definition.initializationOptions?.(ctx),
|
|
14494
15128
|
root: this.opts.root,
|
|
14495
15129
|
read: this.opts.read,
|
|
14496
15130
|
has: this.opts.has,
|
|
@@ -14498,17 +15132,30 @@ var LspPool = class {
|
|
|
14498
15132
|
spawnProcess: this.opts.spawnProcess,
|
|
14499
15133
|
maxOpen: this.opts.maxOpen
|
|
14500
15134
|
}),
|
|
14501
|
-
languages: []
|
|
15135
|
+
languages: [],
|
|
15136
|
+
joins: []
|
|
14502
15137
|
};
|
|
14503
15138
|
this.entries.set(command, entry);
|
|
14504
15139
|
return entry;
|
|
14505
15140
|
}
|
|
14506
15141
|
publish() {
|
|
14507
|
-
if (
|
|
15142
|
+
if (this.closing) return;
|
|
15143
|
+
if (this.publishTimer) {
|
|
15144
|
+
this.publishDue = true;
|
|
15145
|
+
return;
|
|
15146
|
+
}
|
|
15147
|
+
this.opts.onStatus(this.status());
|
|
15148
|
+
this.publishTimer = setTimeout(() => {
|
|
15149
|
+
this.publishTimer = null;
|
|
15150
|
+
if (!this.publishDue || this.closing) return;
|
|
15151
|
+
this.publishDue = false;
|
|
15152
|
+
this.publish();
|
|
15153
|
+
}, PUBLISH_MS);
|
|
15154
|
+
this.publishTimer.unref();
|
|
14508
15155
|
}
|
|
14509
15156
|
};
|
|
14510
15157
|
//#endregion
|
|
14511
|
-
//#region src/server/revspec.ts
|
|
15158
|
+
//#region src/server/mode/revspec.ts
|
|
14512
15159
|
/** The uncommitted tree, accepted on either side of a comparison. */
|
|
14513
15160
|
const WORKTREE = "worktree";
|
|
14514
15161
|
/**
|
|
@@ -17922,7 +18569,7 @@ function imageType(buf) {
|
|
|
17922
18569
|
return null;
|
|
17923
18570
|
}
|
|
17924
18571
|
//#endregion
|
|
17925
|
-
//#region src/server/routes.ts
|
|
18572
|
+
//#region src/server/routes/body.ts
|
|
17926
18573
|
/** Invalid JSON is a request error; failures after parsing still propagate. */
|
|
17927
18574
|
async function readJson(c) {
|
|
17928
18575
|
try {
|
|
@@ -17931,110 +18578,10 @@ async function readJson(c) {
|
|
|
17931
18578
|
throw new HTTPException(400, { message: "invalid JSON body" });
|
|
17932
18579
|
}
|
|
17933
18580
|
}
|
|
17934
|
-
|
|
17935
|
-
|
|
17936
|
-
|
|
17937
|
-
|
|
17938
|
-
if (err instanceof HTTPException) return c.json({ error: err.message }, err.status);
|
|
17939
|
-
if (err instanceof ZodError) return c.json({ error: prettifyError(err) }, 400);
|
|
17940
|
-
if (err instanceof NotFoundError) return c.json({ error: "not found" }, 404);
|
|
17941
|
-
if (err instanceof RevspecError || err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
17942
|
-
if (err instanceof ImportError || err instanceof UnquotableError) return c.json({ error: err.message }, 400);
|
|
17943
|
-
if (err instanceof LspUnavailableError) return c.json({ error: err.message }, 409);
|
|
17944
|
-
if (err instanceof GithubError) return c.json({ error: err.message }, err.status);
|
|
17945
|
-
console.error(err);
|
|
17946
|
-
return c.json({ error: err.message }, 500);
|
|
17947
|
-
});
|
|
17948
|
-
app.use("/api/*", async (c, next) => {
|
|
17949
|
-
if (c.req.path !== "/api/config") await session.ready();
|
|
17950
|
-
await next();
|
|
17951
|
-
});
|
|
17952
|
-
app.get("/api/snapshot", async (c) => c.json(await session.snapshotter.current()));
|
|
17953
|
-
app.get("/api/moved", (c) => c.json(session.moved));
|
|
17954
|
-
app.post("/api/reload", async (c) => c.json(await session.reload()));
|
|
17955
|
-
app.delete("/api/iterations", async (c) => c.json(await session.clearIterations()));
|
|
17956
|
-
app.delete("/api/iterations/:n", async (c) => {
|
|
17957
|
-
const n = Number(c.req.param("n"));
|
|
17958
|
-
if (!Number.isInteger(n) || n < 1) throw new RevspecError(`not an iteration number: ${c.req.param("n")}`);
|
|
17959
|
-
return c.json(await session.deleteIteration(n));
|
|
17960
|
-
});
|
|
17961
|
-
app.post("/api/mode", async (c) => {
|
|
17962
|
-
const req = ModeRequestSchema.parse(await readJson(c));
|
|
17963
|
-
return c.json(await session.switchMode(req));
|
|
17964
|
-
});
|
|
17965
|
-
app.get("/api/github", async (c) => {
|
|
17966
|
-
const snap = await session.snapshotter.current();
|
|
17967
|
-
return c.json(await session.github(snap));
|
|
17968
|
-
});
|
|
17969
|
-
app.get("/api/refs", async (c) => c.json(await session.repo.refs()));
|
|
17970
|
-
app.get("/api/last-commits-preview", async (c) => {
|
|
17971
|
-
const { oldOffset, newOffset } = LastCommitsQuerySchema.parse(c.req.query());
|
|
17972
|
-
return c.json(await session.repo.lastCommitsPreview(oldOffset, newOffset));
|
|
17973
|
-
});
|
|
17974
|
-
app.get("/api/patch", async (c) => {
|
|
17975
|
-
const { path } = PatchQuerySchema.parse(c.req.query());
|
|
17976
|
-
if (!path) return c.text(await session.snapshotter.patchAll());
|
|
17977
|
-
const patch = await session.snapshotter.patch(path);
|
|
17978
|
-
if (patch == null) return c.json({ error: "not a changed file" }, 404);
|
|
17979
|
-
return c.text(patch);
|
|
17980
|
-
});
|
|
17981
|
-
app.post("/api/patch", async (c) => {
|
|
17982
|
-
const { paths } = PatchRequestSchema.parse(await readJson(c));
|
|
17983
|
-
return c.text(await session.snapshotter.patchMany(paths));
|
|
17984
|
-
});
|
|
17985
|
-
app.get("/api/file", async (c) => {
|
|
17986
|
-
const { path, rev } = FileQuerySchema.parse(c.req.query());
|
|
17987
|
-
const snap = await session.snapshotter.current();
|
|
17988
|
-
const buf = await session.readSide(snap, path, rev) ?? (rev === "new" ? await deps.lsp?.readExternal(path) : null);
|
|
17989
|
-
if (buf == null) return c.json({ error: "absent on that side" }, 404);
|
|
17990
|
-
const binary = isBinary(buf);
|
|
17991
|
-
const body = {
|
|
17992
|
-
path,
|
|
17993
|
-
contents: binary ? "" : buf.toString("utf8"),
|
|
17994
|
-
binary
|
|
17995
|
-
};
|
|
17996
|
-
return c.json(body);
|
|
17997
|
-
});
|
|
17998
|
-
app.get("/api/image", async (c) => {
|
|
17999
|
-
const { path, rev, key } = ImageQuerySchema.parse(c.req.query());
|
|
18000
|
-
const buf = await session.readImage(await session.snapshotter.current(), path, rev, key);
|
|
18001
|
-
const type = buf && imageType(buf);
|
|
18002
|
-
if (!type) return c.json({ error: "no image under that key" }, 404);
|
|
18003
|
-
return c.body(new Uint8Array(buf), 200, {
|
|
18004
|
-
"content-type": type,
|
|
18005
|
-
"cache-control": "private, max-age=31536000, immutable",
|
|
18006
|
-
"x-content-type-options": "nosniff",
|
|
18007
|
-
"cross-origin-resource-policy": "same-origin"
|
|
18008
|
-
});
|
|
18009
|
-
});
|
|
18010
|
-
app.get("/api/search", async (c) => {
|
|
18011
|
-
const query = SearchQuerySchema.parse(c.req.query());
|
|
18012
|
-
const { q, scope } = query;
|
|
18013
|
-
const snap = await session.snapshotter.current();
|
|
18014
|
-
let paths = scope === "repo" ? void 0 : scope === "file" ? snap.tree.filter((p) => p === query.path) : snap.changed.filter((f) => f.status !== "D").map((f) => f.path);
|
|
18015
|
-
let ranges;
|
|
18016
|
-
if (query.content === "diff") {
|
|
18017
|
-
const entries = await mapLimit(snap.changed.filter((f) => f.status !== "D" && !f.binary && (!paths || paths.includes(f.path))), 8, async (file) => {
|
|
18018
|
-
const patch = await session.repo.patch(snap.oldSha, snap.newSha, file, snap.context);
|
|
18019
|
-
return [file.path, shownRanges(patch, "new")];
|
|
18020
|
-
});
|
|
18021
|
-
ranges = new Map(entries);
|
|
18022
|
-
paths = entries.filter(([, spans]) => spans.length > 0).map(([path]) => path);
|
|
18023
|
-
}
|
|
18024
|
-
const { matches, truncated } = await session.repo.grep(q, snap.newSha, 500, {
|
|
18025
|
-
word: query.word,
|
|
18026
|
-
ignoreCase: query.i,
|
|
18027
|
-
regex: query.re,
|
|
18028
|
-
paths,
|
|
18029
|
-
ranges
|
|
18030
|
-
});
|
|
18031
|
-
const body = {
|
|
18032
|
-
query: q,
|
|
18033
|
-
matches,
|
|
18034
|
-
truncated
|
|
18035
|
-
};
|
|
18036
|
-
return c.json(body);
|
|
18037
|
-
});
|
|
18581
|
+
//#endregion
|
|
18582
|
+
//#region src/server/routes/comments.ts
|
|
18583
|
+
/** Comment threads, their export to a pending GitHub review, and viewed marks. Each write broadcasts its invalidation. */
|
|
18584
|
+
function commentRoutes(app, { session, hub }) {
|
|
18038
18585
|
app.get("/api/threads", (c) => {
|
|
18039
18586
|
const q = ThreadQuerySchema.parse(c.req.query());
|
|
18040
18587
|
q.state ??= "all";
|
|
@@ -18114,57 +18661,202 @@ function createApi(deps) {
|
|
|
18114
18661
|
hub.broadcast({ type: "viewed" });
|
|
18115
18662
|
return c.json(list);
|
|
18116
18663
|
});
|
|
18664
|
+
}
|
|
18665
|
+
//#endregion
|
|
18666
|
+
//#region src/server/routes/lsp.ts
|
|
18667
|
+
/** Symbol navigation, answered only while the new side is the checkout the language servers read. */
|
|
18668
|
+
function lspRoutes(app, deps) {
|
|
18669
|
+
const { session } = deps;
|
|
18117
18670
|
app.get("/api/lsp/status", (c) => c.json(lspStatus(deps)));
|
|
18118
|
-
|
|
18119
|
-
|
|
18120
|
-
|
|
18671
|
+
app.get("/api/lsp/log", (c) => {
|
|
18672
|
+
const { server } = LspLogQuerySchema.parse(c.req.query());
|
|
18673
|
+
const entries = deps.lsp?.log(server);
|
|
18674
|
+
if (entries == null) return c.json({ error: "no such language server" }, 404);
|
|
18675
|
+
return c.json({ entries });
|
|
18676
|
+
});
|
|
18677
|
+
/**
|
|
18678
|
+
* The pool, or the 409 reason `lspBlocker` gives: off, the wrong side, no server, or
|
|
18679
|
+
* not that feature. An advisory query names no feature: a server not offering it
|
|
18680
|
+
* answers nothing, which is no reason to refuse.
|
|
18681
|
+
*/
|
|
18682
|
+
const lspFor = async ({ path, feature }) => {
|
|
18121
18683
|
const snap = await session.snapshotter.current();
|
|
18122
|
-
|
|
18684
|
+
const error = lspBlocker(lspStatus(deps), snap, {
|
|
18685
|
+
path,
|
|
18686
|
+
feature,
|
|
18687
|
+
patient: true
|
|
18688
|
+
});
|
|
18689
|
+
if (error != null || !deps.lsp) return { error: error ?? "language servers are off" };
|
|
18123
18690
|
if (path != null && !snap.tree.includes(path)) return { error: `${path} is not in the snapshot` };
|
|
18124
18691
|
return { lsp: deps.lsp };
|
|
18125
18692
|
};
|
|
18126
18693
|
app.post("/api/lsp/definition", async (c) => {
|
|
18127
18694
|
const pos = LspPositionSchema.parse(await readJson(c));
|
|
18128
|
-
const r = await lspFor(
|
|
18695
|
+
const r = await lspFor({
|
|
18696
|
+
path: pos.path,
|
|
18697
|
+
feature: "definition"
|
|
18698
|
+
});
|
|
18129
18699
|
if ("error" in r) return c.json({ error: r.error }, 409);
|
|
18130
18700
|
return c.json(await r.lsp.definition(pos));
|
|
18131
18701
|
});
|
|
18132
18702
|
app.post("/api/lsp/type-definition", async (c) => {
|
|
18133
18703
|
const pos = LspPositionSchema.parse(await readJson(c));
|
|
18134
|
-
const r = await lspFor(
|
|
18704
|
+
const r = await lspFor({
|
|
18705
|
+
path: pos.path,
|
|
18706
|
+
feature: "typeDefinition"
|
|
18707
|
+
});
|
|
18135
18708
|
if ("error" in r) return c.json({ error: r.error }, 409);
|
|
18136
18709
|
return c.json(await r.lsp.typeDefinition(pos));
|
|
18137
18710
|
});
|
|
18138
18711
|
app.post("/api/lsp/hover", async (c) => {
|
|
18139
18712
|
const pos = LspPositionSchema.parse(await readJson(c));
|
|
18140
|
-
const r = await lspFor(
|
|
18713
|
+
const r = await lspFor({
|
|
18714
|
+
path: pos.path,
|
|
18715
|
+
feature: "hover"
|
|
18716
|
+
});
|
|
18141
18717
|
if ("error" in r) return c.json({ error: r.error }, 409);
|
|
18142
18718
|
return c.json(await r.lsp.hover(pos));
|
|
18143
18719
|
});
|
|
18144
|
-
app.post("/api/lsp/token-kind", async (c) => {
|
|
18145
|
-
const pos = LspPositionSchema.parse(await readJson(c));
|
|
18146
|
-
const r = await lspFor(pos.path);
|
|
18147
|
-
if ("error" in r) return c.json({ error: r.error }, 409);
|
|
18148
|
-
return c.json(await r.lsp.tokenKind(pos));
|
|
18149
|
-
});
|
|
18150
18720
|
app.post("/api/lsp/references", async (c) => {
|
|
18151
18721
|
const pos = LspPositionSchema.parse(await readJson(c));
|
|
18152
|
-
const r = await lspFor(
|
|
18722
|
+
const r = await lspFor({
|
|
18723
|
+
path: pos.path,
|
|
18724
|
+
feature: "references"
|
|
18725
|
+
});
|
|
18153
18726
|
if ("error" in r) return c.json({ error: r.error }, 409);
|
|
18154
18727
|
return c.json(await r.lsp.references(pos));
|
|
18155
18728
|
});
|
|
18156
18729
|
app.post("/api/lsp/occurrences", async (c) => {
|
|
18157
18730
|
const pos = LspPositionSchema.parse(await readJson(c));
|
|
18158
|
-
const r = await lspFor(pos.path);
|
|
18731
|
+
const r = await lspFor({ path: pos.path });
|
|
18159
18732
|
if ("error" in r) return c.json({ error: r.error }, 409);
|
|
18160
18733
|
return c.json(await r.lsp.occurrences(pos));
|
|
18161
18734
|
});
|
|
18162
18735
|
app.get("/api/lsp/symbols", async (c) => {
|
|
18163
18736
|
const { path, q: query } = SymbolsQuerySchema.parse(c.req.query());
|
|
18164
|
-
const r = await lspFor(
|
|
18737
|
+
const r = await lspFor({
|
|
18738
|
+
path,
|
|
18739
|
+
feature: path != null ? "documentSymbol" : "workspaceSymbol"
|
|
18740
|
+
});
|
|
18165
18741
|
if ("error" in r) return c.json({ error: r.error }, 409);
|
|
18166
18742
|
return c.json(path != null ? await r.lsp.documentSymbols(path) : await r.lsp.workspaceSymbols(query ?? ""));
|
|
18167
18743
|
});
|
|
18744
|
+
}
|
|
18745
|
+
function lspStatus(deps) {
|
|
18746
|
+
return deps.lsp?.status() ?? {
|
|
18747
|
+
enabled: false,
|
|
18748
|
+
servers: [],
|
|
18749
|
+
missing: []
|
|
18750
|
+
};
|
|
18751
|
+
}
|
|
18752
|
+
//#endregion
|
|
18753
|
+
//#region src/server/routes/api.ts
|
|
18754
|
+
function createApi(deps) {
|
|
18755
|
+
const { session, hub } = deps;
|
|
18756
|
+
const app = new Hono();
|
|
18757
|
+
app.onError((err, c) => {
|
|
18758
|
+
if (err instanceof HTTPException) return c.json({ error: err.message }, err.status);
|
|
18759
|
+
if (err instanceof ZodError) return c.json({ error: prettifyError(err) }, 400);
|
|
18760
|
+
if (err instanceof NotFoundError) return c.json({ error: "not found" }, 404);
|
|
18761
|
+
if (err instanceof RevspecError || err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
18762
|
+
if (err instanceof ImportError || err instanceof UnquotableError) return c.json({ error: err.message }, 400);
|
|
18763
|
+
if (err instanceof LspUnavailableError) return c.json({ error: err.message }, 409);
|
|
18764
|
+
if (err instanceof GithubError) return c.json({ error: err.message }, err.status);
|
|
18765
|
+
console.error(err);
|
|
18766
|
+
return c.json({ error: err.message }, 500);
|
|
18767
|
+
});
|
|
18768
|
+
app.use("/api/*", async (c, next) => {
|
|
18769
|
+
if (c.req.path !== "/api/config") await session.ready();
|
|
18770
|
+
await next();
|
|
18771
|
+
});
|
|
18772
|
+
app.get("/api/snapshot", async (c) => c.json(await session.snapshotter.current()));
|
|
18773
|
+
app.get("/api/moved", (c) => c.json(session.moved));
|
|
18774
|
+
app.post("/api/reload", async (c) => c.json(await session.reload()));
|
|
18775
|
+
app.delete("/api/iterations", async (c) => c.json(await session.clearIterations()));
|
|
18776
|
+
app.delete("/api/iterations/:n", async (c) => {
|
|
18777
|
+
const n = Number(c.req.param("n"));
|
|
18778
|
+
if (!Number.isInteger(n) || n < 1) throw new RevspecError(`not an iteration number: ${c.req.param("n")}`);
|
|
18779
|
+
return c.json(await session.deleteIteration(n));
|
|
18780
|
+
});
|
|
18781
|
+
app.post("/api/mode", async (c) => {
|
|
18782
|
+
const req = ModeRequestSchema.parse(await readJson(c));
|
|
18783
|
+
return c.json(await session.switchMode(req));
|
|
18784
|
+
});
|
|
18785
|
+
app.get("/api/github", async (c) => {
|
|
18786
|
+
const snap = await session.snapshotter.current();
|
|
18787
|
+
return c.json(await session.github(snap));
|
|
18788
|
+
});
|
|
18789
|
+
app.get("/api/refs", async (c) => c.json(await session.repo.refs()));
|
|
18790
|
+
app.get("/api/last-commits-preview", async (c) => {
|
|
18791
|
+
const { oldOffset, newOffset } = LastCommitsQuerySchema.parse(c.req.query());
|
|
18792
|
+
return c.json(await session.repo.lastCommitsPreview(oldOffset, newOffset));
|
|
18793
|
+
});
|
|
18794
|
+
app.get("/api/patch", async (c) => {
|
|
18795
|
+
const { path } = PatchQuerySchema.parse(c.req.query());
|
|
18796
|
+
if (!path) return c.text(await session.snapshotter.patchAll());
|
|
18797
|
+
const patch = await session.snapshotter.patch(path);
|
|
18798
|
+
if (patch == null) return c.json({ error: "not a changed file" }, 404);
|
|
18799
|
+
return c.text(patch);
|
|
18800
|
+
});
|
|
18801
|
+
app.post("/api/patch", async (c) => {
|
|
18802
|
+
const { paths } = PatchRequestSchema.parse(await readJson(c));
|
|
18803
|
+
return c.text(await session.snapshotter.patchMany(paths));
|
|
18804
|
+
});
|
|
18805
|
+
app.get("/api/file", async (c) => {
|
|
18806
|
+
const { path, rev } = FileQuerySchema.parse(c.req.query());
|
|
18807
|
+
const snap = await session.snapshotter.current();
|
|
18808
|
+
const buf = await session.readSide(snap, path, rev) ?? (rev === "new" ? await deps.lsp?.readExternal(path) : null);
|
|
18809
|
+
if (buf == null) return c.json({ error: "absent on that side" }, 404);
|
|
18810
|
+
const binary = isBinary(buf);
|
|
18811
|
+
const body = {
|
|
18812
|
+
path,
|
|
18813
|
+
contents: binary ? "" : buf.toString("utf8"),
|
|
18814
|
+
binary
|
|
18815
|
+
};
|
|
18816
|
+
return c.json(body);
|
|
18817
|
+
});
|
|
18818
|
+
app.get("/api/image", async (c) => {
|
|
18819
|
+
const { path, rev, key } = ImageQuerySchema.parse(c.req.query());
|
|
18820
|
+
const buf = await session.readImage(await session.snapshotter.current(), path, rev, key);
|
|
18821
|
+
const type = buf && imageType(buf);
|
|
18822
|
+
if (!type) return c.json({ error: "no image under that key" }, 404);
|
|
18823
|
+
return c.body(new Uint8Array(buf), 200, {
|
|
18824
|
+
"content-type": type,
|
|
18825
|
+
"cache-control": "private, max-age=31536000, immutable",
|
|
18826
|
+
"x-content-type-options": "nosniff",
|
|
18827
|
+
"cross-origin-resource-policy": "same-origin"
|
|
18828
|
+
});
|
|
18829
|
+
});
|
|
18830
|
+
app.get("/api/search", async (c) => {
|
|
18831
|
+
const query = SearchQuerySchema.parse(c.req.query());
|
|
18832
|
+
const { q, scope } = query;
|
|
18833
|
+
const snap = await session.snapshotter.current();
|
|
18834
|
+
let paths = scope === "repo" ? void 0 : scope === "file" ? snap.tree.filter((p) => p === query.path) : snap.changed.filter((f) => f.status !== "D").map((f) => f.path);
|
|
18835
|
+
let ranges;
|
|
18836
|
+
if (query.content === "diff") {
|
|
18837
|
+
const entries = await mapLimit(snap.changed.filter((f) => f.status !== "D" && !f.binary && (!paths || paths.includes(f.path))), 8, async (file) => {
|
|
18838
|
+
const patch = await session.repo.patch(snap.oldSha, snap.newSha, file, snap.context);
|
|
18839
|
+
return [file.path, shownRanges(patch, "new")];
|
|
18840
|
+
});
|
|
18841
|
+
ranges = new Map(entries);
|
|
18842
|
+
paths = entries.filter(([, spans]) => spans.length > 0).map(([path]) => path);
|
|
18843
|
+
}
|
|
18844
|
+
const { matches, truncated } = await session.repo.grep(q, snap.newSha, 500, {
|
|
18845
|
+
word: query.word,
|
|
18846
|
+
ignoreCase: query.i,
|
|
18847
|
+
regex: query.re,
|
|
18848
|
+
paths,
|
|
18849
|
+
ranges
|
|
18850
|
+
});
|
|
18851
|
+
const body = {
|
|
18852
|
+
query: q,
|
|
18853
|
+
matches,
|
|
18854
|
+
truncated
|
|
18855
|
+
};
|
|
18856
|
+
return c.json(body);
|
|
18857
|
+
});
|
|
18858
|
+
commentRoutes(app, deps);
|
|
18859
|
+
lspRoutes(app, deps);
|
|
18168
18860
|
app.get("/api/config", (c) => c.json(effectiveConfig(deps)));
|
|
18169
18861
|
app.put("/api/config", async (c) => {
|
|
18170
18862
|
const body = ConfigUpdateSchema.parse(await readJson(c));
|
|
@@ -18182,14 +18874,7 @@ function effectiveConfig(deps) {
|
|
|
18182
18874
|
return {
|
|
18183
18875
|
...c,
|
|
18184
18876
|
autoViewed: [.../* @__PURE__ */ new Set([...c.autoViewed, ...deps.extraAutoViewed])],
|
|
18185
|
-
contextLines: deps.session.context
|
|
18186
|
-
};
|
|
18187
|
-
}
|
|
18188
|
-
function lspStatus(deps) {
|
|
18189
|
-
return deps.lsp?.status() ?? {
|
|
18190
|
-
enabled: false,
|
|
18191
|
-
servers: [],
|
|
18192
|
-
missing: []
|
|
18877
|
+
contextLines: deps.session.context
|
|
18193
18878
|
};
|
|
18194
18879
|
}
|
|
18195
18880
|
//#endregion
|
|
@@ -18492,7 +19177,7 @@ function parsePullRequest(value) {
|
|
|
18492
19177
|
};
|
|
18493
19178
|
}
|
|
18494
19179
|
//#endregion
|
|
18495
|
-
//#region src/server/
|
|
19180
|
+
//#region src/server/github/metadata.ts
|
|
18496
19181
|
const LOOKUP_TIMEOUT_MS = 5e3;
|
|
18497
19182
|
/**
|
|
18498
19183
|
* Enriches a comparison without fetching commits, changing its endpoints, or changing comment
|
|
@@ -18558,10 +19243,142 @@ async function discoverGithub(repo, snap, { github, prUrl } = {}) {
|
|
|
18558
19243
|
}
|
|
18559
19244
|
}
|
|
18560
19245
|
//#endregion
|
|
18561
|
-
//#region src/server/github/
|
|
19246
|
+
//#region src/server/github/pendingReview.ts
|
|
18562
19247
|
function isFileComment(c) {
|
|
18563
19248
|
return "subjectType" in c;
|
|
18564
19249
|
}
|
|
19250
|
+
/** The anchor must still match before we rewrite an exported thread. */
|
|
19251
|
+
function anchorKey(c) {
|
|
19252
|
+
if (isFileComment(c)) return [c.path, "FILE"].join("\0");
|
|
19253
|
+
return [
|
|
19254
|
+
c.path,
|
|
19255
|
+
c.side,
|
|
19256
|
+
c.startLine ?? c.line,
|
|
19257
|
+
c.line
|
|
19258
|
+
].join("\0");
|
|
19259
|
+
}
|
|
19260
|
+
const PENDING_QUERY = `query($owner:String!,$repo:String!,$number:Int!){
|
|
19261
|
+
viewer{login}
|
|
19262
|
+
repository(owner:$owner,name:$repo){pullRequest(number:$number){id reviews(last:20,states:[PENDING]){nodes{id author{login}}}}}
|
|
19263
|
+
}`;
|
|
19264
|
+
/**
|
|
19265
|
+
* The PR's node id and the viewer's own pending review on it, or null. GitHub hides other
|
|
19266
|
+
* people's pending reviews, and the login check makes sure of it: appending to someone
|
|
19267
|
+
* else's draft would put our comments in their review.
|
|
19268
|
+
*/
|
|
19269
|
+
async function findPendingReview(github, pr) {
|
|
19270
|
+
const { owner, repo } = repoOfPrUrl(pr.url);
|
|
19271
|
+
const data = await github.graphql(PENDING_QUERY, {
|
|
19272
|
+
owner,
|
|
19273
|
+
repo,
|
|
19274
|
+
number: pr.number
|
|
19275
|
+
});
|
|
19276
|
+
const pullRequestId = data.repository?.pullRequest?.id;
|
|
19277
|
+
if (typeof pullRequestId !== "string") throw new GithubError(`cannot read pull request ${pr.url} from GitHub`, 502);
|
|
19278
|
+
const login = data.viewer?.login;
|
|
19279
|
+
for (const node of data.repository?.pullRequest?.reviews?.nodes ?? []) if (node?.id && node.author?.login === login) return {
|
|
19280
|
+
pullRequestId,
|
|
19281
|
+
pending: { id: node.id }
|
|
19282
|
+
};
|
|
19283
|
+
return {
|
|
19284
|
+
pullRequestId,
|
|
19285
|
+
pending: null
|
|
19286
|
+
};
|
|
19287
|
+
}
|
|
19288
|
+
const THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$after:String){
|
|
19289
|
+
repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100,after:$after){
|
|
19290
|
+
pageInfo{hasNextPage endCursor}
|
|
19291
|
+
nodes{path line startLine diffSide subjectType comments(first:1){nodes{id body pullRequestReview{id}}}}
|
|
19292
|
+
}}}
|
|
19293
|
+
}`;
|
|
19294
|
+
/**
|
|
19295
|
+
* The pending review's own comments, by anchor. It has to be `reviewThreads`: it is the only view
|
|
19296
|
+
* that reports a *pending* comment's real position — REST's review-comment list leaves `line`,
|
|
19297
|
+
* `side` and `start_line` null for one, and the GraphQL comment type has no side field at all.
|
|
19298
|
+
* A failed or incomplete lookup aborts the export: absence is safe to infer only from a complete read.
|
|
19299
|
+
*/
|
|
19300
|
+
async function pendingComments(github, pr, reviewId) {
|
|
19301
|
+
const by = /* @__PURE__ */ new Map();
|
|
19302
|
+
const { owner, repo } = repoOfPrUrl(pr.url);
|
|
19303
|
+
let after = null;
|
|
19304
|
+
for (let page = 0; page < 10; page++) {
|
|
19305
|
+
const threads = (await github.graphql(THREADS_QUERY, {
|
|
19306
|
+
owner,
|
|
19307
|
+
repo,
|
|
19308
|
+
number: pr.number,
|
|
19309
|
+
after
|
|
19310
|
+
})).repository?.pullRequest?.reviewThreads;
|
|
19311
|
+
if (!threads?.nodes || typeof threads.pageInfo?.hasNextPage !== "boolean") throw new GithubError("cannot read the complete pending review", 502);
|
|
19312
|
+
for (const t of threads.nodes) {
|
|
19313
|
+
const c = t?.comments?.nodes?.[0];
|
|
19314
|
+
if (!t?.path || !c?.id || c.pullRequestReview?.id !== reviewId) continue;
|
|
19315
|
+
const key = pendingKey(t);
|
|
19316
|
+
if (key == null) continue;
|
|
19317
|
+
const at = by.get(key);
|
|
19318
|
+
if (at) at.push({
|
|
19319
|
+
nodeId: c.id,
|
|
19320
|
+
body: c.body ?? ""
|
|
19321
|
+
});
|
|
19322
|
+
else by.set(key, [{
|
|
19323
|
+
nodeId: c.id,
|
|
19324
|
+
body: c.body ?? ""
|
|
19325
|
+
}]);
|
|
19326
|
+
}
|
|
19327
|
+
if (!threads.pageInfo.hasNextPage) return by;
|
|
19328
|
+
if (!threads.pageInfo.endCursor || threads.pageInfo.endCursor === after) throw new GithubError("cannot read the next page of the pending review", 502);
|
|
19329
|
+
after = threads.pageInfo.endCursor;
|
|
19330
|
+
}
|
|
19331
|
+
throw new GithubError("pending review exceeds the 1000-thread lookup limit; export stopped without changes", 502);
|
|
19332
|
+
}
|
|
19333
|
+
/** The anchor key of a thread already in the review, or null for one GitHub reports without a position. */
|
|
19334
|
+
function pendingKey(t) {
|
|
19335
|
+
const path = t.path;
|
|
19336
|
+
if (t.subjectType === "FILE") return anchorKey({
|
|
19337
|
+
path,
|
|
19338
|
+
subjectType: "FILE",
|
|
19339
|
+
body: ""
|
|
19340
|
+
});
|
|
19341
|
+
if (t.line == null) return null;
|
|
19342
|
+
return anchorKey({
|
|
19343
|
+
path,
|
|
19344
|
+
side: t.diffSide === "LEFT" ? "LEFT" : "RIGHT",
|
|
19345
|
+
line: t.line,
|
|
19346
|
+
startLine: t.startLine ?? void 0,
|
|
19347
|
+
body: ""
|
|
19348
|
+
});
|
|
19349
|
+
}
|
|
19350
|
+
const UPDATE_COMMENT = `mutation($input:UpdatePullRequestReviewCommentInput!){updatePullRequestReviewComment(input:$input){pullRequestReviewComment{id}}}`;
|
|
19351
|
+
/** Rewrites one comment where it already sits in the pending review. */
|
|
19352
|
+
async function updateComment(github, nodeId, body) {
|
|
19353
|
+
await github.graphql(UPDATE_COMMENT, { input: {
|
|
19354
|
+
pullRequestReviewCommentId: nodeId,
|
|
19355
|
+
body
|
|
19356
|
+
} });
|
|
19357
|
+
}
|
|
19358
|
+
const CREATE_REVIEW = `mutation($input:AddPullRequestReviewInput!){addPullRequestReview(input:$input){pullRequestReview{id}}}`;
|
|
19359
|
+
/**
|
|
19360
|
+
* No `event` in the input, so GitHub keeps the new review pending with all of its threads.
|
|
19361
|
+
* Resolves the review's node id, for the comments a new review cannot carry.
|
|
19362
|
+
*/
|
|
19363
|
+
async function createPendingReview(github, pullRequestId, commitOID, threads) {
|
|
19364
|
+
const id = (await github.graphql(CREATE_REVIEW, { input: {
|
|
19365
|
+
pullRequestId,
|
|
19366
|
+
commitOID,
|
|
19367
|
+
threads
|
|
19368
|
+
} })).addPullRequestReview?.pullRequestReview?.id;
|
|
19369
|
+
if (typeof id !== "string") throw new GithubError("GitHub did not return the created review", 502);
|
|
19370
|
+
return { id };
|
|
19371
|
+
}
|
|
19372
|
+
const ADD_THREAD = `mutation($input:AddPullRequestReviewThreadInput!){addPullRequestReviewThread(input:$input){thread{id}}}`;
|
|
19373
|
+
/** Appends one thread to the pending review; the only way to extend one, or to place a file comment. */
|
|
19374
|
+
async function addToPendingReview(github, reviewId, c) {
|
|
19375
|
+
await github.graphql(ADD_THREAD, { input: {
|
|
19376
|
+
pullRequestReviewId: reviewId,
|
|
19377
|
+
...c
|
|
19378
|
+
} });
|
|
19379
|
+
}
|
|
19380
|
+
//#endregion
|
|
19381
|
+
//#region src/server/github/review.ts
|
|
18565
19382
|
/**
|
|
18566
19383
|
* Threads → the comments of one pending GitHub review, one per thread. Stale threads are skipped:
|
|
18567
19384
|
* their lines no longer sit in the diff, and GitHub would refuse them anyway. With
|
|
@@ -18697,10 +19514,7 @@ async function exportToGithub({ snap, pullRequest: pr, threads, threadIds, githu
|
|
|
18697
19514
|
});
|
|
18698
19515
|
continue;
|
|
18699
19516
|
}
|
|
18700
|
-
await github.
|
|
18701
|
-
pullRequestReviewCommentId: hit.nodeId,
|
|
18702
|
-
body: c.body
|
|
18703
|
-
} });
|
|
19517
|
+
await updateComment(github, hit.nodeId, c.body);
|
|
18704
19518
|
updated++;
|
|
18705
19519
|
}
|
|
18706
19520
|
for (const c of add) {
|
|
@@ -18723,129 +19537,6 @@ async function exportToGithub({ snap, pullRequest: pr, threads, threadIds, githu
|
|
|
18723
19537
|
function threadMarker(id) {
|
|
18724
19538
|
return `<!-- diffle-thread:${encodeURIComponent(id)} -->`;
|
|
18725
19539
|
}
|
|
18726
|
-
/** The anchor must still match before we rewrite an exported thread. */
|
|
18727
|
-
function anchorKey(c) {
|
|
18728
|
-
if (isFileComment(c)) return [c.path, "FILE"].join("\0");
|
|
18729
|
-
return [
|
|
18730
|
-
c.path,
|
|
18731
|
-
c.side,
|
|
18732
|
-
c.startLine ?? c.line,
|
|
18733
|
-
c.line
|
|
18734
|
-
].join("\0");
|
|
18735
|
-
}
|
|
18736
|
-
const PENDING_QUERY = `query($owner:String!,$repo:String!,$number:Int!){
|
|
18737
|
-
viewer{login}
|
|
18738
|
-
repository(owner:$owner,name:$repo){pullRequest(number:$number){id reviews(last:20,states:[PENDING]){nodes{id author{login}}}}}
|
|
18739
|
-
}`;
|
|
18740
|
-
/**
|
|
18741
|
-
* The PR's node id and the viewer's own pending review on it, or null. GitHub hides other
|
|
18742
|
-
* people's pending reviews, and the login check makes sure of it: appending to someone
|
|
18743
|
-
* else's draft would put our comments in their review.
|
|
18744
|
-
*/
|
|
18745
|
-
async function findPendingReview(github, pr) {
|
|
18746
|
-
const { owner, repo } = repoOfPrUrl(pr.url);
|
|
18747
|
-
const data = await github.graphql(PENDING_QUERY, {
|
|
18748
|
-
owner,
|
|
18749
|
-
repo,
|
|
18750
|
-
number: pr.number
|
|
18751
|
-
});
|
|
18752
|
-
const pullRequestId = data.repository?.pullRequest?.id;
|
|
18753
|
-
if (typeof pullRequestId !== "string") throw new GithubError(`cannot read pull request ${pr.url} from GitHub`, 502);
|
|
18754
|
-
const login = data.viewer?.login;
|
|
18755
|
-
for (const node of data.repository?.pullRequest?.reviews?.nodes ?? []) if (node?.id && node.author?.login === login) return {
|
|
18756
|
-
pullRequestId,
|
|
18757
|
-
pending: { id: node.id }
|
|
18758
|
-
};
|
|
18759
|
-
return {
|
|
18760
|
-
pullRequestId,
|
|
18761
|
-
pending: null
|
|
18762
|
-
};
|
|
18763
|
-
}
|
|
18764
|
-
const THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$after:String){
|
|
18765
|
-
repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100,after:$after){
|
|
18766
|
-
pageInfo{hasNextPage endCursor}
|
|
18767
|
-
nodes{path line startLine diffSide subjectType comments(first:1){nodes{id body pullRequestReview{id}}}}
|
|
18768
|
-
}}}
|
|
18769
|
-
}`;
|
|
18770
|
-
/**
|
|
18771
|
-
* The pending review's own comments, by anchor. It has to be `reviewThreads`: it is the only view
|
|
18772
|
-
* that reports a *pending* comment's real position — REST's review-comment list leaves `line`,
|
|
18773
|
-
* `side` and `start_line` null for one, and the GraphQL comment type has no side field at all.
|
|
18774
|
-
* A failed or incomplete lookup aborts the export: absence is safe to infer only from a complete read.
|
|
18775
|
-
*/
|
|
18776
|
-
async function pendingComments(github, pr, reviewId) {
|
|
18777
|
-
const by = /* @__PURE__ */ new Map();
|
|
18778
|
-
const { owner, repo } = repoOfPrUrl(pr.url);
|
|
18779
|
-
let after = null;
|
|
18780
|
-
for (let page = 0; page < 10; page++) {
|
|
18781
|
-
const threads = (await github.graphql(THREADS_QUERY, {
|
|
18782
|
-
owner,
|
|
18783
|
-
repo,
|
|
18784
|
-
number: pr.number,
|
|
18785
|
-
after
|
|
18786
|
-
})).repository?.pullRequest?.reviewThreads;
|
|
18787
|
-
if (!threads?.nodes || typeof threads.pageInfo?.hasNextPage !== "boolean") throw new GithubError("cannot read the complete pending review", 502);
|
|
18788
|
-
for (const t of threads.nodes) {
|
|
18789
|
-
const c = t?.comments?.nodes?.[0];
|
|
18790
|
-
if (!t?.path || !c?.id || c.pullRequestReview?.id !== reviewId) continue;
|
|
18791
|
-
const key = pendingKey(t);
|
|
18792
|
-
if (key == null) continue;
|
|
18793
|
-
const at = by.get(key);
|
|
18794
|
-
if (at) at.push({
|
|
18795
|
-
nodeId: c.id,
|
|
18796
|
-
body: c.body ?? ""
|
|
18797
|
-
});
|
|
18798
|
-
else by.set(key, [{
|
|
18799
|
-
nodeId: c.id,
|
|
18800
|
-
body: c.body ?? ""
|
|
18801
|
-
}]);
|
|
18802
|
-
}
|
|
18803
|
-
if (!threads.pageInfo.hasNextPage) return by;
|
|
18804
|
-
if (!threads.pageInfo.endCursor || threads.pageInfo.endCursor === after) throw new GithubError("cannot read the next page of the pending review", 502);
|
|
18805
|
-
after = threads.pageInfo.endCursor;
|
|
18806
|
-
}
|
|
18807
|
-
throw new GithubError("pending review exceeds the 1000-thread lookup limit; export stopped without changes", 502);
|
|
18808
|
-
}
|
|
18809
|
-
/** The anchor key of a thread already in the review, or null for one GitHub reports without a position. */
|
|
18810
|
-
function pendingKey(t) {
|
|
18811
|
-
const path = t.path;
|
|
18812
|
-
if (t.subjectType === "FILE") return anchorKey({
|
|
18813
|
-
path,
|
|
18814
|
-
subjectType: "FILE",
|
|
18815
|
-
body: ""
|
|
18816
|
-
});
|
|
18817
|
-
if (t.line == null) return null;
|
|
18818
|
-
return anchorKey({
|
|
18819
|
-
path,
|
|
18820
|
-
side: t.diffSide === "LEFT" ? "LEFT" : "RIGHT",
|
|
18821
|
-
line: t.line,
|
|
18822
|
-
startLine: t.startLine ?? void 0,
|
|
18823
|
-
body: ""
|
|
18824
|
-
});
|
|
18825
|
-
}
|
|
18826
|
-
const UPDATE_COMMENT = `mutation($input:UpdatePullRequestReviewCommentInput!){updatePullRequestReviewComment(input:$input){pullRequestReviewComment{id}}}`;
|
|
18827
|
-
const CREATE_REVIEW = `mutation($input:AddPullRequestReviewInput!){addPullRequestReview(input:$input){pullRequestReview{id}}}`;
|
|
18828
|
-
/**
|
|
18829
|
-
* No `event` in the input, so GitHub keeps the new review pending with all of its threads.
|
|
18830
|
-
* Resolves the review's node id, for the comments a new review cannot carry.
|
|
18831
|
-
*/
|
|
18832
|
-
async function createPendingReview(github, pullRequestId, commitOID, threads) {
|
|
18833
|
-
const id = (await github.graphql(CREATE_REVIEW, { input: {
|
|
18834
|
-
pullRequestId,
|
|
18835
|
-
commitOID,
|
|
18836
|
-
threads
|
|
18837
|
-
} })).addPullRequestReview?.pullRequestReview?.id;
|
|
18838
|
-
if (typeof id !== "string") throw new GithubError("GitHub did not return the created review", 502);
|
|
18839
|
-
return { id };
|
|
18840
|
-
}
|
|
18841
|
-
const ADD_THREAD = `mutation($input:AddPullRequestReviewThreadInput!){addPullRequestReviewThread(input:$input){thread{id}}}`;
|
|
18842
|
-
/** Appends one thread to the pending review; the only way to extend one, or to place a file comment. */
|
|
18843
|
-
async function addToPendingReview(github, reviewId, c) {
|
|
18844
|
-
await github.graphql(ADD_THREAD, { input: {
|
|
18845
|
-
pullRequestReviewId: reviewId,
|
|
18846
|
-
...c
|
|
18847
|
-
} });
|
|
18848
|
-
}
|
|
18849
19540
|
function describe(skipped) {
|
|
18850
19541
|
const counts = /* @__PURE__ */ new Map();
|
|
18851
19542
|
for (const s of skipped) counts.set(s.reason, (counts.get(s.reason) ?? 0) + 1);
|
|
@@ -18948,7 +19639,7 @@ async function read(file) {
|
|
|
18948
19639
|
}
|
|
18949
19640
|
}
|
|
18950
19641
|
//#endregion
|
|
18951
|
-
//#region src/server/rangediff.ts
|
|
19642
|
+
//#region src/server/mode/rangediff.ts
|
|
18952
19643
|
/**
|
|
18953
19644
|
* The mode that compares `older` with `newer`: `older`'s head replayed onto `newer`'s base against
|
|
18954
19645
|
* `newer`'s head, so only what the series itself changed in between shows, with the two ranges'
|
|
@@ -19009,7 +19700,7 @@ async function pairRanges(repo, older, newer) {
|
|
|
19009
19700
|
}));
|
|
19010
19701
|
}
|
|
19011
19702
|
//#endregion
|
|
19012
|
-
//#region src/server/mode.ts
|
|
19703
|
+
//#region src/server/mode/resolve.ts
|
|
19013
19704
|
/**
|
|
19014
19705
|
* Resolves a comparison to Git endpoints for initial mode setup, PR setup after
|
|
19015
19706
|
* fetching, and snapshot refreshes. Revisions become full hashes; "worktree"
|
|
@@ -20108,6 +20799,7 @@ function normalize(c) {
|
|
|
20108
20799
|
autoViewed: Array.isArray(c.autoViewed) ? c.autoViewed.filter((p) => typeof p === "string").map((p) => p.trim()).filter(Boolean) : DEFAULT_USER_CONFIG.autoViewed,
|
|
20109
20800
|
contextLines: typeof c.contextLines === "number" && Number.isFinite(c.contextLines) ? Math.max(0, Math.min(1e4, Math.floor(c.contextLines))) : DEFAULT_USER_CONFIG.contextLines,
|
|
20110
20801
|
followRefs: FollowRefsSchema.safeParse(c.followRefs).data ?? DEFAULT_USER_CONFIG.followRefs,
|
|
20802
|
+
lineWrap: typeof c.lineWrap === "boolean" ? c.lineWrap : DEFAULT_USER_CONFIG.lineWrap,
|
|
20111
20803
|
lspCommands: lspCommands(c)
|
|
20112
20804
|
};
|
|
20113
20805
|
}
|
|
@@ -24059,7 +24751,8 @@ program.command("lsp").description("show the language server each language would
|
|
|
24059
24751
|
const { servers, missing } = resolveServers(LANGUAGE_IDS, lspCommands);
|
|
24060
24752
|
const answer = /* @__PURE__ */ new Map();
|
|
24061
24753
|
for (const s of servers) for (const language of s.languages) answer.set(language, `${s.command}${lspCommands[language] == null ? "" : c.dim(" (config)")}`);
|
|
24062
|
-
for (const m of missing) answer.set(m.language, c.dim(m.tried.length ? `not on PATH (tried ${m.tried.join(", ")})` : "off in config"));
|
|
24754
|
+
for (const m of missing) answer.set(m.language, c.dim(m.why ?? (m.tried.length ? `not on PATH (tried ${m.tried.join(", ")})` : "off in config")));
|
|
24755
|
+
for (const s of servers) for (const language of s.joins) answer.set(language, `${answer.get(language) ?? ""} ${c.dim("+")} ${s.command}`);
|
|
24063
24756
|
const width = Math.max(...LANGUAGE_IDS.map((l) => l.length));
|
|
24064
24757
|
for (const language of LANGUAGE_IDS) console.log(`${language.padEnd(width)} ${answer.get(language) ?? ""}`);
|
|
24065
24758
|
});
|
|
@@ -24176,7 +24869,7 @@ async function serve(req, opts, repo, closeRepo, config, github, timing) {
|
|
|
24176
24869
|
if (snap.mode.live !== "none") console.error(`👀 ${c.dim(snap.mode.live === "worktree" ? "watching the worktree" : session.follows ? "watching refs" : "watching refs; moved refs show a Reload button")}${opts.watch ? "" : c.dim(" (disabled with --no-watch)")}`);
|
|
24177
24870
|
if (lsp) {
|
|
24178
24871
|
const { servers, missing } = lsp.status();
|
|
24179
|
-
const names = servers.map((s) => `${s.name} ${c.dim(`(${s
|
|
24872
|
+
const names = servers.map((s) => `${s.name} ${c.dim(`(${languagesLabel(s)})`)}`).join(", ");
|
|
24180
24873
|
if (!followsCheckout(snap)) console.error(`🧭 ${c.dim("no language server: symbol navigation needs the new side to be the checkout")}`);
|
|
24181
24874
|
else console.error(`🧭 ${c.dim("lsp")} ${names || c.dim("no language server for this diff")}`);
|
|
24182
24875
|
for (const m of missing.filter((m) => m.tried.length)) console.error(`${c.yellow("!")} no ${m.language} language server ${c.dim(`(tried ${m.tried.join(", ")})`)}`);
|
|
@@ -24202,17 +24895,11 @@ async function connectGithub() {
|
|
|
24202
24895
|
}
|
|
24203
24896
|
/**
|
|
24204
24897
|
* The pool reads new-side text through the session, so the snapshot allowlist applies to
|
|
24205
|
-
* every LSP path. Each snapshot decides which languages run
|
|
24206
|
-
*
|
|
24207
|
-
* (see LspPool.track). Nothing starts when the new side is not the checkout: the client
|
|
24208
|
-
* refuses symbol navigation there, and a server would otherwise see text that contradicts
|
|
24209
|
-
* the disk it indexes.
|
|
24898
|
+
* every LSP path. Each snapshot decides which languages run and which files the servers
|
|
24899
|
+
* hold open (see LspPool.track).
|
|
24210
24900
|
*/
|
|
24211
24901
|
function startLsp(repo, session, hub, overrides, preload) {
|
|
24212
|
-
session.onSnapshot((snap) =>
|
|
24213
|
-
const paths = followsCheckout(snap) ? snap.changed.filter((f) => f.status !== "D" && !f.binary).map((f) => f.path) : [];
|
|
24214
|
-
lsp.track(paths);
|
|
24215
|
-
});
|
|
24902
|
+
session.onSnapshot((snap) => void lsp.track(snap));
|
|
24216
24903
|
const reported = /* @__PURE__ */ new Map();
|
|
24217
24904
|
const lsp = new LspPool({
|
|
24218
24905
|
root: repo.root,
|