@mrclrchtr/supi-lsp 10.0.0 → 11.0.1

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.
Files changed (38) hide show
  1. package/README.md +88 -2
  2. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/LICENSE +21 -0
  3. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/README.md +743 -0
  4. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/index.js +17 -0
  5. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/constants.js +184 -0
  6. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/parse.js +1425 -0
  7. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/picomatch.js +361 -0
  8. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/scan.js +403 -0
  9. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/utils.js +72 -0
  10. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/package.json +83 -0
  11. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/posix.js +3 -0
  12. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +4 -1
  13. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +11 -0
  14. package/node_modules/@mrclrchtr/supi-code-runtime/src/file-associations.ts +190 -0
  15. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +22 -0
  16. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  17. package/node_modules/ignore/README.md +61 -6
  18. package/node_modules/ignore/index.d.ts +55 -20
  19. package/node_modules/ignore/index.js +93 -67
  20. package/node_modules/ignore/legacy.js +109 -85
  21. package/node_modules/ignore/package.json +1 -1
  22. package/package.json +3 -3
  23. package/src/api.ts +1 -0
  24. package/src/client/client-diagnostic-host.ts +3 -0
  25. package/src/client/client-diagnostics.ts +6 -4
  26. package/src/client/client-request-enrollment.ts +66 -22
  27. package/src/client/client-semantic-input-barrier.ts +37 -5
  28. package/src/client/client.ts +136 -43
  29. package/src/manager/manager.ts +261 -22
  30. package/src/provider/refactor-planning.ts +13 -0
  31. package/src/provider/semantic-edit-discovery.ts +151 -0
  32. package/src/provider/semantic-edit-normalizer.ts +97 -129
  33. package/src/provider/semantic-edit-protocol.ts +132 -0
  34. package/src/session/runtime-controller.ts +8 -1
  35. package/src/session/runtime-registry.ts +7 -20
  36. package/src/session/runtime-rename.ts +172 -0
  37. package/src/session/workspace-lsp-runtime.ts +14 -0
  38. package/src/utils.ts +12 -11
@@ -0,0 +1,190 @@
1
+ import * as path from "node:path";
2
+ import picomatch from "picomatch";
3
+
4
+ /** Language selected for a C-family source file. */
5
+ export type CFamilyLanguage = "c" | "cpp";
6
+
7
+ /** One ordered, project-relative language rule for an existing C-family file. */
8
+ export interface CFamilyFileAssociationRule {
9
+ readonly pattern: string;
10
+ readonly language: CFamilyLanguage;
11
+ }
12
+
13
+ /** C-family suffixes that already have Tree-sitter and LSP support. */
14
+ export const C_FAMILY_EXTENSIONS = Object.freeze([
15
+ ".c",
16
+ ".h",
17
+ ".cpp",
18
+ ".hpp",
19
+ ".cc",
20
+ ".cxx",
21
+ ".hxx",
22
+ ".c++",
23
+ ".h++",
24
+ ] as const);
25
+
26
+ export type CFamilyExtension = (typeof C_FAMILY_EXTENSIONS)[number];
27
+
28
+ const DEFAULT_C_FAMILY_LANGUAGES: Readonly<Record<CFamilyExtension, CFamilyLanguage>> =
29
+ Object.freeze({
30
+ ".c": "c",
31
+ ".h": "c",
32
+ ".cpp": "cpp",
33
+ ".hpp": "cpp",
34
+ ".cc": "cpp",
35
+ ".cxx": "cpp",
36
+ ".hxx": "cpp",
37
+ ".c++": "cpp",
38
+ ".h++": "cpp",
39
+ });
40
+
41
+ /** Immutable C-family language selection for one workspace. */
42
+ export interface CFamilyFileAssociationResolver {
43
+ /** Absolute root used to make project-relative paths. */
44
+ readonly workspaceRoot: string;
45
+ /** Validated ordered rules captured when the workspace host starts. */
46
+ readonly rules: readonly CFamilyFileAssociationRule[];
47
+ /** Select the configured language or the existing suffix default. */
48
+ languageForFile(filePath: string): CFamilyLanguage | undefined;
49
+ }
50
+
51
+ /** Return the current default language for one supported C-family suffix. */
52
+ export function defaultCFamilyLanguage(extension: string): CFamilyLanguage | undefined {
53
+ return DEFAULT_C_FAMILY_LANGUAGES[extension.toLowerCase() as CFamilyExtension];
54
+ }
55
+
56
+ /**
57
+ * Validate and compile ordered C-family file associations for one workspace.
58
+ * Patterns are case-sensitive globs over POSIX-style, workspace-relative paths.
59
+ * Supported patterns use `*`, `?`, `**`, character classes, and brace alternatives.
60
+ * The first matching rule wins. Rules never expand the eligible suffix set.
61
+ *
62
+ * @throws If the rule list, a rule, a language, or a pattern is invalid.
63
+ */
64
+ export function createCFamilyFileAssociationResolver(
65
+ workspaceRoot: string,
66
+ input: unknown = [],
67
+ ): CFamilyFileAssociationResolver {
68
+ const rules = validateRules(input);
69
+ const matchers = rules.map((rule, index) => ({
70
+ language: rule.language,
71
+ matches: compilePattern(rule.pattern, `code-intelligence.fileAssociations[${index}]`),
72
+ }));
73
+ const root = path.resolve(workspaceRoot);
74
+ return Object.freeze({
75
+ workspaceRoot: root,
76
+ rules,
77
+ languageForFile(filePath: string): CFamilyLanguage | undefined {
78
+ const extension = normalizeExtension(filePath);
79
+ const fallback = defaultCFamilyLanguage(extension);
80
+ if (!fallback) return undefined;
81
+
82
+ const absolutePath = path.resolve(root, filePath);
83
+ const relativePath = toPosixPath(path.relative(root, absolutePath));
84
+ if (
85
+ relativePath === ".." ||
86
+ relativePath.startsWith("../") ||
87
+ path.posix.isAbsolute(relativePath)
88
+ ) {
89
+ return fallback;
90
+ }
91
+ for (const rule of matchers) {
92
+ if (rule.matches(relativePath)) return rule.language;
93
+ }
94
+ return fallback;
95
+ },
96
+ });
97
+ }
98
+
99
+ function validateRules(input: unknown): readonly CFamilyFileAssociationRule[] {
100
+ if (!Array.isArray(input)) {
101
+ throw new Error('Invalid "code-intelligence.fileAssociations": expected an ordered array.');
102
+ }
103
+ if (input.length > 64) {
104
+ throw new Error('Invalid "code-intelligence.fileAssociations": use no more than 64 rules.');
105
+ }
106
+ const rules: CFamilyFileAssociationRule[] = [];
107
+ const seenPatterns = new Set<string>();
108
+ for (const [index, value] of input.entries()) {
109
+ const key = `code-intelligence.fileAssociations[${index}]`;
110
+ if (!isRecord(value) || !hasExactKeys(value, ["pattern", "language"])) {
111
+ throw new Error(`${key} must contain only "pattern" and "language" fields.`);
112
+ }
113
+ if (typeof value.pattern !== "string") {
114
+ throw new Error(`${key}.pattern must be a string.`);
115
+ }
116
+ if (value.language !== "c" && value.language !== "cpp") {
117
+ throw new Error(`${key}.language must be "c" or "cpp".`);
118
+ }
119
+ if (seenPatterns.has(value.pattern)) {
120
+ throw new Error(`${key}.pattern duplicates an earlier rule.`);
121
+ }
122
+ seenPatterns.add(value.pattern);
123
+ rules.push(Object.freeze({ pattern: value.pattern, language: value.language }));
124
+ }
125
+ return Object.freeze(rules);
126
+ }
127
+
128
+ function compilePattern(pattern: string, key: string): (file: string) => boolean {
129
+ if (
130
+ pattern.length === 0 ||
131
+ pattern.length > 512 ||
132
+ pattern !== pattern.trim() ||
133
+ pattern.includes("\\") ||
134
+ hasControlCharacter(pattern) ||
135
+ pattern.startsWith("/") ||
136
+ /^[A-Za-z]:/.test(pattern) ||
137
+ pattern.startsWith("!")
138
+ ) {
139
+ throw new Error(`${key}.pattern must be a safe workspace-relative glob using "/" separators.`);
140
+ }
141
+ const segments = pattern.split("/");
142
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
143
+ throw new Error(`${key}.pattern contains an unsafe or unsupported path pattern.`);
144
+ }
145
+ try {
146
+ return picomatch(pattern, {
147
+ nocase: false,
148
+ windows: false,
149
+ dot: true,
150
+ strictBrackets: true,
151
+ nonegate: true,
152
+ noextglob: true,
153
+ maxLength: 512,
154
+ });
155
+ } catch (error) {
156
+ throw new Error(`${key}.pattern is not a valid glob: ${errorMessage(error)}`, { cause: error });
157
+ }
158
+ }
159
+
160
+ function hasControlCharacter(value: string): boolean {
161
+ for (const character of value) {
162
+ const code = character.charCodeAt(0);
163
+ if (code < 32 || code === 127) return true;
164
+ }
165
+ return false;
166
+ }
167
+
168
+ function normalizeExtension(filePath: string): string {
169
+ return path.posix.extname(toPosixPath(filePath)).toLowerCase();
170
+ }
171
+
172
+ function toPosixPath(value: string): string {
173
+ return value.replaceAll("\\", "/");
174
+ }
175
+
176
+ function isRecord(value: unknown): value is Record<string, unknown> {
177
+ return typeof value === "object" && value !== null && !Array.isArray(value);
178
+ }
179
+
180
+ function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
181
+ const allowed = new Set(keys);
182
+ return (
183
+ keys.every((key) => Object.hasOwn(value, key)) &&
184
+ Object.keys(value).every((key) => allowed.has(key))
185
+ );
186
+ }
187
+
188
+ function errorMessage(error: unknown): string {
189
+ return error instanceof Error ? error.message : String(error);
190
+ }
@@ -80,6 +80,14 @@ export interface DocumentCodeSymbol extends CodeSymbol {
80
80
  */
81
81
  export type CodeResult<T> =
82
82
  | { kind: "success"; data: T }
83
+ | {
84
+ kind: "partial";
85
+ data: T;
86
+ file: string;
87
+ reason: string;
88
+ /** Syntax error and missing-node ranges in 0-based UTF-16 coordinates. */
89
+ errorRanges: SourceRange[];
90
+ }
83
91
  | { kind: "unsupported-language"; file: string; message: string }
84
92
  | { kind: "file-access-error"; file: string; message: string }
85
93
  | { kind: "validation-error"; message: string }
@@ -93,7 +101,10 @@ export type ConfidenceMode = "semantic" | "structural" | "heuristic" | "unavaila
93
101
 
94
102
  export interface OutlineData {
95
103
  name: string;
104
+ /** Common structural search category. */
96
105
  kind: string;
106
+ /** Source-language declaration kind, separate from common search category. */
107
+ nativeKind?: string;
97
108
  /** Exact 1-based start of the declaration name token, when known. */
98
109
  nameAnchor?: SymbolAnchor;
99
110
  /** Exclusive 1-based end of a verified literal name span, when known. */
@@ -156,6 +167,12 @@ export interface FileEdit {
156
167
  /** A document-state precondition established when semantic edits are normalized. */
157
168
  export type DocumentEditPrecondition =
158
169
  | { file: string; kind: "open-document-version"; version: number }
170
+ | {
171
+ file: string;
172
+ kind: "open-document-content";
173
+ version: number;
174
+ fingerprint: string;
175
+ }
159
176
  | { file: string; kind: "disk-content" };
160
177
 
161
178
  /**
@@ -257,5 +274,10 @@ export interface CalleesData {
257
274
  /** A single call-site match with name and start line. */
258
275
  export interface CallSite {
259
276
  name: string;
277
+ /** 1-based UTF-16 start position of the written callee expression. */
260
278
  startLine: number;
279
+ startCharacter: number;
280
+ /** Exclusive 1-based UTF-16 end position of the captured source expression. */
281
+ endLine: number;
282
+ endCharacter: number;
261
283
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "10.0.0",
3
+ "version": "11.0.1",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -173,6 +173,29 @@ ig.ignores(path.join('./abc')) // path.join('./abc') -> 'abc'
173
173
 
174
174
  In other words, each `Pathname` here should be a relative path to the directory of the gitignore rules.
175
175
 
176
+ A `.` or `..` component anywhere else in a `Pathname` is outside the
177
+ convention as well, though no error is thrown for it: the check reads how the
178
+ path starts, so that it costs nothing on a hot path. `path.relative()` never
179
+ produces such a component, and one that carries it is not matched the way
180
+ `git` matches it — `git` drops the component from the path first, `ignore`
181
+ matches the path as it was given.
182
+
183
+ ```js
184
+ // path.relative('/', '/a/./b') -> 'a/b'
185
+
186
+ // Right
187
+ ignore().add('a/b').ignores('a/b') // true
188
+
189
+ // Outside the convention: git matches 'a/b' here and so says true
190
+ ignore().add('a/b').ignores('a/./b') // false
191
+
192
+ // Outside the convention: git matches 'a' here and so says false
193
+ ignore().add('a/.').ignores('a/.') // true
194
+ ```
195
+
196
+ A rule that matches an ancestor still agrees, since the component makes no
197
+ difference to it: `add('a').ignores('a/./b')` is `true` either way.
198
+
176
199
  Suppose the dir structure is:
177
200
 
178
201
  ```
@@ -265,7 +288,8 @@ Returns `TestResult`
265
288
  // Since 5.0.0
266
289
  interface TestResult {
267
290
  ignored: boolean
268
- // true if the `pathname` is finally unignored by some negative pattern
291
+ // true if a negative pattern matched the `pathname` itself, and no parent
292
+ // directory of it is ignored -- see "When both are false" below
269
293
  unignored: boolean
270
294
  // The `IgnoreRule` which ignores the pathname
271
295
  rule?: IgnoreRule
@@ -283,8 +307,39 @@ interface IgnoreRule {
283
307
  ```
284
308
 
285
309
  - `{ignored: true, unignored: false}`: the `pathname` is ignored
286
- - `{ignored: false, unignored: true}`: the `pathname` is unignored
287
- - `{ignored: false, unignored: false}`: the `pathname` is never matched by any ignore rules.
310
+ - `{ignored: false, unignored: true}`: a negative pattern matched the `pathname` itself
311
+ - `{ignored: false, unignored: false}`: no rule matched the `pathname` itself, and no parent directory of it is ignored
312
+
313
+ ### When both are `false`
314
+
315
+ `{ignored: false, unignored: false}` covers two different situations:
316
+
317
+ - no rule matched the `pathname`, nor any of its parent directories, or
318
+ - no rule matched the `pathname` itself, but one of its parent directories is unignored by a negative pattern: the `pathname` is unignored **indirectly**.
319
+
320
+ `unignored` stays `false` in the second case because only `ignored` passes down from a parent directory. A `pathname` whose parent directory is ignored is ignored as well — git never re-includes a file whose parent directory is excluded — but one whose parent directory is unignored is decided on its own, by the rules that match the `pathname` itself.
321
+
322
+ ```js
323
+ const ig2 = ignore().add([
324
+ 'foo*',
325
+ '!foobar'
326
+ ])
327
+
328
+ // The directory `foobar/` is unignored directly, by `!foobar`
329
+ ig2.test('foobar/') // {ignored: false, unignored: true}
330
+
331
+ // No rule matches `foobar/x.js` itself. It is not ignored only because its
332
+ // parent directory `foobar/` is not, so it is unignored indirectly
333
+ ig2.test('foobar/x.js') // {ignored: false, unignored: false}
334
+ ```
335
+
336
+ To tell the two situations apart, test the parent directories of the `pathname`, each with a trailing slash: the `pathname` is unignored indirectly if any of them is `unignored`.
337
+
338
+ ```js
339
+ ['foobar/'].some(dir => ig2.test(dir).unignored) // true
340
+ ```
341
+
342
+ `unignored` is meant for packages that implement their own caching mechanism upon `node-ignore`. To simply tell whether a `pathname` is ignored, use `.ignores()` or check `TestResult::ignored`.
288
343
 
289
344
  ## .checkIgnore(target: string): TestResult
290
345
 
@@ -294,7 +349,7 @@ Debugs gitignore / exclude files, which is equivalent to `git check-ignore -v`.
294
349
 
295
350
  - **target** `string` the target to test.
296
351
 
297
- Returns `TestResult`
352
+ Returns `TestResult`, the same shape `.test()` returns, so `{ignored: false, unignored: false}` carries the [same two meanings](#when-both-are-false) here.
298
353
 
299
354
  ```js
300
355
  ig.add({
@@ -305,10 +360,10 @@ ig.add({
305
360
  const {
306
361
  ignored,
307
362
  rule
308
- } = checkIgnore('foo/')
363
+ } = ig.checkIgnore('foo/')
309
364
 
310
365
  if (ignored) {
311
- console.log(`.gitignore:${result}:${rule.mark}:${rule.pattern} foo/`)
366
+ console.log(`.gitignore:${rule.mark}:${rule.pattern} foo/`)
312
367
  }
313
368
 
314
369
  // .gitignore:60:foo/* foo/
@@ -1,27 +1,61 @@
1
- type Pathname = string
2
-
3
- interface IgnoreRule {
4
- pattern: string
5
- mark?: string
6
- negative: boolean
7
- }
8
-
9
- interface TestResult {
10
- ignored: boolean
11
- unignored: boolean
12
- rule?: IgnoreRule
13
- }
14
-
15
- interface PatternParams {
16
- pattern: string
17
- mark?: string
18
- }
19
-
20
1
  /**
21
2
  * Creates new ignore manager.
22
3
  */
23
4
  declare function ignore(options?: ignore.Options): ignore.Ignore
24
5
  declare namespace ignore {
6
+ /**
7
+ * A path relative to the directory the rules belong to, as
8
+ * `path.relative()` returns it.
9
+ */
10
+ type Pathname = string
11
+
12
+ interface PatternParams {
13
+ pattern: string
14
+ /**
15
+ * Carried through to `IgnoreRule.mark`, for packages that build upon
16
+ * `ignore`.
17
+ */
18
+ mark?: string
19
+ }
20
+
21
+ interface IgnoreRule {
22
+ /**
23
+ * The original pattern.
24
+ */
25
+ pattern: string
26
+ /**
27
+ * Carried through from `PatternParams.mark`.
28
+ */
29
+ mark?: string
30
+ /**
31
+ * Whether the pattern is a negative one, such as `!foo`.
32
+ */
33
+ negative: boolean
34
+ }
35
+
36
+ interface TestResult {
37
+ /**
38
+ * Whether the pathname is ignored, by a rule or because a parent
39
+ * directory of it is.
40
+ */
41
+ ignored: boolean
42
+ /**
43
+ * True if a negative pattern matched the pathname itself, and no parent
44
+ * directory of it is ignored.
45
+ *
46
+ * Only `ignored` passes down from a parent directory, so a pathname that
47
+ * is not ignored only because a parent directory was unignored keeps
48
+ * this false, as does one no rule applies to at all. To tell those two
49
+ * apart, test the parent directories, each with a trailing slash.
50
+ */
51
+ unignored: boolean
52
+ /**
53
+ * The rule that ignores the pathname. Present exactly when `ignored` is
54
+ * true.
55
+ */
56
+ rule?: IgnoreRule
57
+ }
58
+
25
59
  interface Ignore {
26
60
  /**
27
61
  * Adds one or several rules to the current manager.
@@ -54,7 +88,8 @@ declare namespace ignore {
54
88
  ignores(pathname: Pathname): boolean
55
89
 
56
90
  /**
57
- * Returns whether pathname should be ignored or unignored
91
+ * Tests the pathname against the rules. `ignored` and `unignored` can
92
+ * both be false: see `TestResult.unignored` for the two cases.
58
93
  * @param {string} pathname a path to check
59
94
  * @returns TestResult
60
95
  */