@aprimediet/codewalker 1.0.0 → 1.2.0
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 +44 -50
- package/index.ts +6 -42
- package/package.json +20 -39
- package/prompts/codewalker.md +7 -0
- package/skills/codewalker/SKILL.md +43 -0
- package/src/cards.test.ts +88 -0
- package/src/cards.ts +87 -0
- package/src/db.test.ts +343 -0
- package/src/db.ts +363 -0
- package/src/extract/ctags-parse.test.ts +108 -0
- package/src/extract/ctags-parse.ts +112 -0
- package/src/extract/ctags.ts +51 -0
- package/src/extract/docs.test.ts +81 -0
- package/src/extract/docs.ts +169 -0
- package/src/extract/regex.test.ts +202 -0
- package/src/extract/regex.ts +192 -0
- package/src/format.test.ts +123 -0
- package/src/format.ts +69 -0
- package/src/git.test.ts +75 -0
- package/src/git.ts +62 -0
- package/src/index.contract.test.ts +145 -0
- package/src/index.ts +173 -0
- package/src/indexer.test.ts +138 -0
- package/src/indexer.ts +352 -0
- package/src/libs/cards.test.ts +86 -0
- package/src/libs/cards.ts +53 -0
- package/src/libs/dts.test.ts +269 -0
- package/src/libs/dts.ts +213 -0
- package/src/libs/indexer.test.ts +236 -0
- package/src/libs/indexer.ts +291 -0
- package/src/libs/resolve.test.ts +218 -0
- package/src/libs/resolve.ts +120 -0
- package/src/project.test.ts +115 -0
- package/src/project.ts +206 -0
- package/src/query.test.ts +169 -0
- package/src/query.ts +89 -0
- package/src/sync.test.ts +116 -0
- package/src/types.ts +117 -0
- package/vitest.config.ts +28 -0
- package/LICENSE +0 -21
- package/agents.ts +0 -126
- package/compat.ts +0 -217
- package/detect.ts +0 -188
- package/docs/PRD.md +0 -78
- package/prd.ts +0 -106
- package/skills/learn-this/SKILL.md +0 -325
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseCtagsLine, parseCtagsOutput, mapCtagsKind, type CtagsTag } from './ctags-parse.ts';
|
|
3
|
+
|
|
4
|
+
describe('mapCtagsKind', () => {
|
|
5
|
+
it('maps ctags kind to SymbolKind', () => {
|
|
6
|
+
expect(mapCtagsKind('function')).toBe('function');
|
|
7
|
+
expect(mapCtagsKind('variable')).toBe('const');
|
|
8
|
+
expect(mapCtagsKind('class')).toBe('class');
|
|
9
|
+
expect(mapCtagsKind('member')).toBe('method');
|
|
10
|
+
expect(mapCtagsKind('enum')).toBe('enum');
|
|
11
|
+
expect(mapCtagsKind('typedef')).toBe('type');
|
|
12
|
+
expect(mapCtagsKind('interface')).toBe('interface');
|
|
13
|
+
expect(mapCtagsKind('namespace')).toBe('namespace');
|
|
14
|
+
expect(mapCtagsKind('module')).toBe('module');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns the kind as-is for unknown kinds', () => {
|
|
18
|
+
expect(mapCtagsKind('macro')).toBe('macro');
|
|
19
|
+
expect(mapCtagsKind('unknown')).toBe('unknown');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('parseCtagsLine', () => {
|
|
24
|
+
it('parses a valid ctags JSON line into a CtagsTag', () => {
|
|
25
|
+
const line = '{"_type":"tag","name":"myFunction","path":"src/file.ts","pattern":"/^export function myFunction()/","line":10,"kind":"function","signature":"(param: string)"}';
|
|
26
|
+
const result = parseCtagsLine(line);
|
|
27
|
+
expect(result).not.toBeNull();
|
|
28
|
+
expect(result!.name).toBe('myFunction');
|
|
29
|
+
expect(result!.path).toBe('src/file.ts');
|
|
30
|
+
expect(result!.line).toBe(10);
|
|
31
|
+
expect(result!.kind).toBe('function');
|
|
32
|
+
expect(result!.signature).toBe('(param: string)');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns null for non-tag lines', () => {
|
|
36
|
+
expect(parseCtagsLine('{"_type":"other","name":"foo"}')).toBeNull();
|
|
37
|
+
expect(parseCtagsLine('not json')).toBeNull();
|
|
38
|
+
expect(parseCtagsLine('')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('tolerates missing signature field', () => {
|
|
42
|
+
const line = '{"_type":"tag","name":"foo","path":"a.ts","line":5,"kind":"function"}';
|
|
43
|
+
const result = parseCtagsLine(line);
|
|
44
|
+
expect(result).not.toBeNull();
|
|
45
|
+
expect(result!.signature).toBe('');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('tolerates missing kind field', () => {
|
|
49
|
+
const line = '{"_type":"tag","name":"foo","path":"a.ts","line":5}';
|
|
50
|
+
const result = parseCtagsLine(line);
|
|
51
|
+
expect(result).not.toBeNull();
|
|
52
|
+
expect(result!.kind).toBe('unknown');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('handles numeric kind field (ctags numeric kind)', () => {
|
|
56
|
+
const line = '{"_type":"tag","name":"Foo","path":"a.ts","line":10,"kind":"class"}';
|
|
57
|
+
const result = parseCtagsLine(line);
|
|
58
|
+
expect(result).not.toBeNull();
|
|
59
|
+
expect(result!.kind).toBe('class');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('parseCtagsOutput', () => {
|
|
64
|
+
it('parses multi-line ctags JSON output into Symbol array', () => {
|
|
65
|
+
const output = [
|
|
66
|
+
'{"_type":"tag","name":"myFunc","path":"src/a.ts","line":5,"kind":"function","signature":"()"}',
|
|
67
|
+
'{"_type":"tag","name":"MyClass","path":"src/a.ts","line":20,"kind":"class","signature":""}',
|
|
68
|
+
'{"_type":"tag","name":"MY_CONST","path":"src/b.ts","line":1,"kind":"variable","signature":"42"}',
|
|
69
|
+
].join('\n');
|
|
70
|
+
|
|
71
|
+
const symbols = parseCtagsOutput(output, '/root/project');
|
|
72
|
+
expect(symbols).toHaveLength(3);
|
|
73
|
+
|
|
74
|
+
expect(symbols[0]!.name).toBe('myFunc');
|
|
75
|
+
expect(symbols[0]!.kind).toBe('function');
|
|
76
|
+
expect(symbols[0]!.file_path).toBe('/root/project/src/a.ts');
|
|
77
|
+
expect(symbols[0]!.line_start).toBe(5);
|
|
78
|
+
|
|
79
|
+
expect(symbols[1]!.name).toBe('MyClass');
|
|
80
|
+
expect(symbols[1]!.kind).toBe('class');
|
|
81
|
+
|
|
82
|
+
expect(symbols[2]!.name).toBe('MY_CONST');
|
|
83
|
+
expect(symbols[2]!.kind).toBe('const');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('skips malformed and non-tag lines', () => {
|
|
87
|
+
const output = [
|
|
88
|
+
'{"_type":"tag","name":"valid","path":"a.ts","line":1,"kind":"function","signature":""}',
|
|
89
|
+
'not json at all',
|
|
90
|
+
'{"_type":"notag","name":"skip"}',
|
|
91
|
+
'',
|
|
92
|
+
'{"_type":"tag","name":"valid2","path":"a.ts","line":5,"kind":"class","signature":""}',
|
|
93
|
+
].join('\n');
|
|
94
|
+
|
|
95
|
+
const symbols = parseCtagsOutput(output, '/root');
|
|
96
|
+
expect(symbols).toHaveLength(2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('returns empty array for empty output', () => {
|
|
100
|
+
expect(parseCtagsOutput('', '/root')).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('resolves relative paths against project root', () => {
|
|
104
|
+
const output = '{"_type":"tag","name":"f","path":"src/lib/util.ts","line":3,"kind":"function","signature":""}';
|
|
105
|
+
const symbols = parseCtagsOutput(output, '/home/user/project');
|
|
106
|
+
expect(symbols[0]!.file_path).toBe('/home/user/project/src/lib/util.ts');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse Universal Ctags JSON output into Symbol[].
|
|
3
|
+
*
|
|
4
|
+
* Universal Ctags emits one JSON object per line with `--output-format=json`.
|
|
5
|
+
* Fields: _type, name, path, pattern, line, kind, signature.
|
|
6
|
+
*
|
|
7
|
+
* This module is PURE — no I/O, takes strings and returns objects.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Symbol, SymbolKind } from "../types.ts";
|
|
11
|
+
|
|
12
|
+
/** Raw ctags tag as parsed from a JSON line. */
|
|
13
|
+
export interface CtagsTag {
|
|
14
|
+
name: string;
|
|
15
|
+
path: string;
|
|
16
|
+
line: number;
|
|
17
|
+
kind: string;
|
|
18
|
+
signature: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Map a ctags `kind` string to our canonical SymbolKind.
|
|
23
|
+
* Unknown kinds are returned as-is so callers can handle them or skip.
|
|
24
|
+
*/
|
|
25
|
+
export function mapCtagsKind(kind: string): string {
|
|
26
|
+
const mapping: Record<string, string> = {
|
|
27
|
+
function: "function",
|
|
28
|
+
variable: "const",
|
|
29
|
+
class: "class",
|
|
30
|
+
member: "method",
|
|
31
|
+
enum: "enum",
|
|
32
|
+
typedef: "type",
|
|
33
|
+
interface: "interface",
|
|
34
|
+
namespace: "namespace",
|
|
35
|
+
module: "module",
|
|
36
|
+
};
|
|
37
|
+
return mapping[kind] ?? kind;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a single ctags JSON line into a CtagsTag, or null if it's not a tag line.
|
|
42
|
+
*/
|
|
43
|
+
export function parseCtagsLine(line: string): CtagsTag | null {
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
if (!trimmed) return null;
|
|
46
|
+
|
|
47
|
+
let parsed: Record<string, unknown>;
|
|
48
|
+
try {
|
|
49
|
+
parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Only process _type === "tag"
|
|
55
|
+
if (parsed["_type"] !== "tag") return null;
|
|
56
|
+
|
|
57
|
+
const name = parsed["name"];
|
|
58
|
+
const path = parsed["path"];
|
|
59
|
+
const lineNum = parsed["line"];
|
|
60
|
+
const kind = parsed["kind"];
|
|
61
|
+
const signature = parsed["signature"];
|
|
62
|
+
|
|
63
|
+
if (typeof name !== "string" || typeof path !== "string") return null;
|
|
64
|
+
if (typeof lineNum !== "number") return null;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
name,
|
|
68
|
+
path,
|
|
69
|
+
line: lineNum,
|
|
70
|
+
kind: typeof kind === "string" ? kind : "unknown",
|
|
71
|
+
signature: typeof signature === "string" ? signature : "",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse multi-line ctags JSON output, returning Symbol[] with absolute paths.
|
|
77
|
+
*
|
|
78
|
+
* @param output - The raw stdout from ctags (one JSON object per line).
|
|
79
|
+
* @param projectRoot - Absolute path to the project root, used to resolve relative file paths.
|
|
80
|
+
*/
|
|
81
|
+
export function parseCtagsOutput(output: string, projectRoot: string): Symbol[] {
|
|
82
|
+
if (!output.trim()) return [];
|
|
83
|
+
|
|
84
|
+
const lines = output.split("\n");
|
|
85
|
+
const symbols: Symbol[] = [];
|
|
86
|
+
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
const tag = parseCtagsLine(line);
|
|
89
|
+
if (!tag) continue;
|
|
90
|
+
|
|
91
|
+
// Resolve relative paths against project root
|
|
92
|
+
const filePath = tag.path.startsWith("/")
|
|
93
|
+
? tag.path
|
|
94
|
+
: `${projectRoot.replace(/\/+$/, "")}/${tag.path}`;
|
|
95
|
+
|
|
96
|
+
symbols.push({
|
|
97
|
+
name: tag.name,
|
|
98
|
+
kind: mapCtagsKind(tag.kind) as SymbolKind,
|
|
99
|
+
file_path: filePath,
|
|
100
|
+
line_start: tag.line,
|
|
101
|
+
line_end: tag.line, // ctags only gives start line
|
|
102
|
+
signature: tag.signature,
|
|
103
|
+
doc: "",
|
|
104
|
+
summary: "",
|
|
105
|
+
card_path: "",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return symbols;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin shell: detect ctags on PATH and run it.
|
|
3
|
+
*
|
|
4
|
+
* Separated from ctags-parse.ts (PURE parsing) so the I/O boundary is explicit.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detect whether ctags is available on PATH.
|
|
11
|
+
*/
|
|
12
|
+
export function detectCtags(): boolean {
|
|
13
|
+
try {
|
|
14
|
+
execSync("ctags --version", { stdio: "ignore", timeout: 5000 });
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Run ctags on a list of files and return raw JSON output.
|
|
23
|
+
*/
|
|
24
|
+
export function runCtags(files: string[], projectRoot: string): string {
|
|
25
|
+
const fileList = files.join(" ");
|
|
26
|
+
return execSync(
|
|
27
|
+
`ctags --output-format=json --fields=+nKzS -f - ${fileList}`,
|
|
28
|
+
{
|
|
29
|
+
cwd: projectRoot,
|
|
30
|
+
encoding: "utf-8",
|
|
31
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
32
|
+
timeout: 30000,
|
|
33
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Run ctags on a single file and return raw JSON output.
|
|
40
|
+
*/
|
|
41
|
+
export function runCtagsOnFile(filePath: string, projectRoot: string): string {
|
|
42
|
+
return execSync(
|
|
43
|
+
`ctags --output-format=json --fields=+nKzS -f - "${filePath}"`,
|
|
44
|
+
{
|
|
45
|
+
cwd: projectRoot,
|
|
46
|
+
encoding: "utf-8",
|
|
47
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
48
|
+
timeout: 10000,
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { extractDocComment } from './docs.ts';
|
|
3
|
+
|
|
4
|
+
describe('extractDocComment', () => {
|
|
5
|
+
it('extracts a JSDoc block comment above a given line', () => {
|
|
6
|
+
const source = `/**
|
|
7
|
+
* This is a doc comment for myFunc.
|
|
8
|
+
* It has multiple lines.
|
|
9
|
+
*/
|
|
10
|
+
function myFunc() {}
|
|
11
|
+
`;
|
|
12
|
+
const doc = extractDocComment(source, 4);
|
|
13
|
+
expect(doc).toContain('This is a doc comment for myFunc');
|
|
14
|
+
expect(doc).toContain('It has multiple lines');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('extracts // line comment block above a given line', () => {
|
|
18
|
+
const source = `// This is a line comment
|
|
19
|
+
// that spans two lines
|
|
20
|
+
function myFunc() {}
|
|
21
|
+
`;
|
|
22
|
+
const doc = extractDocComment(source, 3);
|
|
23
|
+
expect(doc).toContain('This is a line comment');
|
|
24
|
+
expect(doc).toContain('that spans two lines');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('extracts a Python docstring above a given line', () => {
|
|
28
|
+
const source = `"""This is a Python docstring
|
|
29
|
+
for my function.
|
|
30
|
+
"""
|
|
31
|
+
def my_func():
|
|
32
|
+
pass
|
|
33
|
+
`;
|
|
34
|
+
const doc = extractDocComment(source, 4);
|
|
35
|
+
expect(doc).toContain('This is a Python docstring');
|
|
36
|
+
expect(doc).toContain('for my function.');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns empty string when there is no doc comment', () => {
|
|
40
|
+
const source = `const x = 1;
|
|
41
|
+
function myFunc() {}
|
|
42
|
+
`;
|
|
43
|
+
const doc = extractDocComment(source, 2);
|
|
44
|
+
expect(doc).toBe('');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns empty string for a symbol on the first line with no preceding comments', () => {
|
|
48
|
+
const source = `function first() {}`;
|
|
49
|
+
const doc = extractDocComment(source, 1);
|
|
50
|
+
expect(doc).toBe('');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('extracts mixed // and /* */ comment lines', () => {
|
|
54
|
+
const source = `// A brief explanation
|
|
55
|
+
/* Then a block comment */
|
|
56
|
+
function mixed() {}
|
|
57
|
+
`;
|
|
58
|
+
const doc = extractDocComment(source, 3);
|
|
59
|
+
expect(doc).toContain('A brief explanation');
|
|
60
|
+
expect(doc).toContain('Then a block comment');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('stops at blank lines when collecting adjacent comments', () => {
|
|
64
|
+
const source = `// Not adjacent
|
|
65
|
+
|
|
66
|
+
// Directly above
|
|
67
|
+
function myFunc() {}
|
|
68
|
+
`;
|
|
69
|
+
const doc = extractDocComment(source, 4);
|
|
70
|
+
expect(doc).toContain('Directly above');
|
|
71
|
+
expect(doc).not.toContain('Not adjacent');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('handles single-line Python docstrings', () => {
|
|
75
|
+
const source = `"""Single line docstring."""
|
|
76
|
+
def quick(): pass
|
|
77
|
+
`;
|
|
78
|
+
const doc = extractDocComment(source, 2);
|
|
79
|
+
expect(doc).toContain('Single line docstring.');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract leading doc comments from source code.
|
|
3
|
+
*
|
|
4
|
+
* Given a source file and the line number of a symbol, walk upward collecting
|
|
5
|
+
* adjacent comment lines. PURE module — no I/O.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract the doc comment block immediately above a given line in source.
|
|
10
|
+
* Returns the concatenated text of the comment(s), or "" if none.
|
|
11
|
+
*/
|
|
12
|
+
export function extractDocComment(source: string, symbolLine: number): string {
|
|
13
|
+
if (symbolLine <= 1) return "";
|
|
14
|
+
|
|
15
|
+
const lines = source.split("\n");
|
|
16
|
+
const parts: string[] = [];
|
|
17
|
+
|
|
18
|
+
// Walk upward from the line just above the symbol
|
|
19
|
+
let i = symbolLine - 2; // 0-based
|
|
20
|
+
if (i < 0) return "";
|
|
21
|
+
|
|
22
|
+
while (i >= 0) {
|
|
23
|
+
const rawLine = lines[i] as string;
|
|
24
|
+
const trimmed = rawLine.trim();
|
|
25
|
+
|
|
26
|
+
// Blank line stops collection
|
|
27
|
+
if (!trimmed) break;
|
|
28
|
+
|
|
29
|
+
// Line comment (//)
|
|
30
|
+
if (trimmed.startsWith("//")) {
|
|
31
|
+
parts.unshift(trimmed.replace(/^\/\/\s*/, ""));
|
|
32
|
+
i--;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Block comment handling
|
|
37
|
+
const isBlockOpen = trimmed.startsWith("/*") || trimmed.startsWith("/**");
|
|
38
|
+
const isBlockClose = trimmed.endsWith("*/");
|
|
39
|
+
|
|
40
|
+
// Block comment continuation line (starts with *)
|
|
41
|
+
// These appear between /* and */, typically "* text" or "*"
|
|
42
|
+
const isBlockContent = /^\*\s?/.test(trimmed) && !isBlockOpen && !isBlockClose;
|
|
43
|
+
|
|
44
|
+
if (isBlockClose) {
|
|
45
|
+
// Line contains `*/`
|
|
46
|
+
const content = extractStarContent(trimmed);
|
|
47
|
+
if (content) parts.unshift(content);
|
|
48
|
+
if (isBlockOpen) {
|
|
49
|
+
// Single line: /** ... */
|
|
50
|
+
i--;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// Multi-line: enter block content mode
|
|
54
|
+
// Continue walking up
|
|
55
|
+
i--;
|
|
56
|
+
while (i >= 0) {
|
|
57
|
+
const aboveTrimmed = (lines[i] as string).trim();
|
|
58
|
+
if (!aboveTrimmed) break;
|
|
59
|
+
if (aboveTrimmed.startsWith("//")) {
|
|
60
|
+
// Line comment ends the block, insert it and stop
|
|
61
|
+
parts.unshift(aboveTrimmed.replace(/^\/\/\s*/, ""));
|
|
62
|
+
i--;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (aboveTrimmed.startsWith("/*") || aboveTrimmed.startsWith("/**")) {
|
|
66
|
+
// Found the opening
|
|
67
|
+
const openContent = extractStarContent(aboveTrimmed);
|
|
68
|
+
if (openContent) parts.unshift(openContent);
|
|
69
|
+
i--;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
// Block content line: * text or *
|
|
73
|
+
if (/^\*\s?/.test(aboveTrimmed)) {
|
|
74
|
+
const c = aboveTrimmed.replace(/^\*\s?/, "").trim();
|
|
75
|
+
if (c) parts.unshift(c);
|
|
76
|
+
i--;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// Not block content — stop
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (isBlockContent) {
|
|
86
|
+
// Encountered a * continuation line without having seen */
|
|
87
|
+
// This means the */ is below us (closer to the symbol)
|
|
88
|
+
// Walk DOWN to find it, then walk back UP
|
|
89
|
+
// Actually, simpler: just treat * lines as content and walk up
|
|
90
|
+
const content = trimmed.replace(/^\*\s?/, "").trim();
|
|
91
|
+
if (content) parts.unshift(content);
|
|
92
|
+
i--;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (isBlockOpen) {
|
|
97
|
+
// Found opening without a close marker — extract and continue
|
|
98
|
+
const content = extractStarContent(trimmed);
|
|
99
|
+
if (content) parts.unshift(content);
|
|
100
|
+
i--;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Python docstring (""" or ''')
|
|
105
|
+
if (trimmed.startsWith('"""') || trimmed.startsWith("'''")) {
|
|
106
|
+
const quote = trimmed.startsWith('"""') ? '"""' : "'''";
|
|
107
|
+
|
|
108
|
+
// Single-line: """text"""
|
|
109
|
+
if (trimmed.startsWith(quote) && trimmed.endsWith(quote) && trimmed.length > quote.length) {
|
|
110
|
+
const inner = trimmed.slice(quote.length, -quote.length).trim();
|
|
111
|
+
if (inner) parts.unshift(inner);
|
|
112
|
+
i--;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Multi-line opening
|
|
117
|
+
if (trimmed.startsWith(quote) && !trimmed.endsWith(quote)) {
|
|
118
|
+
const afterQuote = trimmed.slice(quote.length).trim();
|
|
119
|
+
if (afterQuote) parts.unshift(afterQuote);
|
|
120
|
+
i--;
|
|
121
|
+
// Walk up to find closing quote
|
|
122
|
+
while (i >= 0) {
|
|
123
|
+
const aboveTrimmed = (lines[i] as string).trim();
|
|
124
|
+
if (aboveTrimmed.endsWith(quote)) {
|
|
125
|
+
const beforeQuote = aboveTrimmed.slice(0, -quote.length).trim();
|
|
126
|
+
if (beforeQuote) parts.unshift(beforeQuote);
|
|
127
|
+
i--;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
parts.unshift(aboveTrimmed);
|
|
131
|
+
i--;
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Just """ on its own line (closing or opening a multi-line)
|
|
137
|
+
if (trimmed === quote) {
|
|
138
|
+
// This is the closing """ — walk up to find the opening
|
|
139
|
+
i--;
|
|
140
|
+
while (i >= 0) {
|
|
141
|
+
const aboveTrimmed = (lines[i] as string).trim();
|
|
142
|
+
if (aboveTrimmed.startsWith(quote)) {
|
|
143
|
+
const afterQuote = aboveTrimmed.slice(quote.length).trim();
|
|
144
|
+
if (afterQuote) parts.unshift(afterQuote);
|
|
145
|
+
i--;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
parts.unshift(aboveTrimmed);
|
|
149
|
+
i--;
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Not a comment — stop
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return parts.join("\n").trim();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Extract visible content from a line inside a /* or /** block comment. */
|
|
163
|
+
function extractStarContent(line: string): string {
|
|
164
|
+
return line
|
|
165
|
+
.replace(/^\/\**\s*/, "") // strip /* or /**
|
|
166
|
+
.replace(/\s*\*\/$/, "") // strip */
|
|
167
|
+
.replace(/^\*\s?/, "") // strip leading *
|
|
168
|
+
.trim();
|
|
169
|
+
}
|