@takara-ai/miru-code 0.1.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/LICENSE +21 -0
- package/README.md +176 -0
- package/bin/miru.cjs +41 -0
- package/package.json +72 -0
- package/src/agents/claude.md +41 -0
- package/src/agents/copilot.md +41 -0
- package/src/agents/cursor.md +40 -0
- package/src/agents/gemini.md +43 -0
- package/src/agents/kiro.md +43 -0
- package/src/agents/opencode.md +44 -0
- package/src/agents.ts +30 -0
- package/src/cache.ts +141 -0
- package/src/chunking/chunking.ts +36 -0
- package/src/chunking/lines.ts +79 -0
- package/src/chunking/structural.ts +244 -0
- package/src/cli.ts +245 -0
- package/src/concurrency.ts +58 -0
- package/src/embeddings/openai.ts +442 -0
- package/src/env-files.ts +44 -0
- package/src/env.ts +50 -0
- package/src/git.ts +52 -0
- package/src/index/bm25-types.ts +18 -0
- package/src/index/bm25-worker.ts +48 -0
- package/src/index/bm25.ts +238 -0
- package/src/index/create.ts +195 -0
- package/src/index/dense.ts +105 -0
- package/src/index/file-walker.ts +113 -0
- package/src/index/files.ts +126 -0
- package/src/index/persistence.ts +100 -0
- package/src/index/quantize.ts +168 -0
- package/src/index/semantic-index.ts +11 -0
- package/src/index/sparse.ts +25 -0
- package/src/index/topk.ts +52 -0
- package/src/index/vector-storage.ts +27 -0
- package/src/index.ts +3 -0
- package/src/mcp/index-cache.ts +95 -0
- package/src/mcp/serve.ts +24 -0
- package/src/mcp/server.ts +100 -0
- package/src/miru-index.ts +269 -0
- package/src/ranking/boosting.ts +329 -0
- package/src/ranking/penalties.ts +110 -0
- package/src/ranking/weighting.ts +11 -0
- package/src/search.ts +180 -0
- package/src/tokens.ts +27 -0
- package/src/types.ts +47 -0
- package/src/utils.ts +64 -0
package/src/cache.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { resolveEmbeddingDimensions, resolveEmbeddingModel } from "./embeddings/openai.ts";
|
|
5
|
+
import {
|
|
6
|
+
loadBm25,
|
|
7
|
+
loadSemantic,
|
|
8
|
+
pathsExist,
|
|
9
|
+
persistencePaths,
|
|
10
|
+
semanticIndexMatchesStorage,
|
|
11
|
+
} from "./index/persistence.ts";
|
|
12
|
+
import { resolveSemanticStorage, semanticStorageFromMetadata } from "./index/vector-storage.ts";
|
|
13
|
+
import type { Chunk, ContentType } from "./types.ts";
|
|
14
|
+
import { chunkFromDict } from "./types.ts";
|
|
15
|
+
|
|
16
|
+
export function resolveCacheFolder(): string {
|
|
17
|
+
const name = "miru";
|
|
18
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
19
|
+
let base: string;
|
|
20
|
+
if (process.platform === "win32") {
|
|
21
|
+
base = process.env.LOCALAPPDATA ?? process.env.APPDATA ?? join(home, "AppData", "Local");
|
|
22
|
+
base = join(base, name, "Cache");
|
|
23
|
+
} else if (process.platform === "darwin") {
|
|
24
|
+
base = join(home, "Library", "Caches", name);
|
|
25
|
+
} else {
|
|
26
|
+
base = process.env.XDG_CACHE_HOME
|
|
27
|
+
? join(process.env.XDG_CACHE_HOME, name)
|
|
28
|
+
: join(home, ".cache", name);
|
|
29
|
+
}
|
|
30
|
+
return base;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function findIndexCachePath(path: string): string {
|
|
34
|
+
const normalized = resolve(path);
|
|
35
|
+
const subdir = createHash("sha256").update(normalized, "utf-8").digest("hex");
|
|
36
|
+
return join(resolveCacheFolder(), subdir, "index");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function metadataMatches(
|
|
40
|
+
metadata: Record<string, unknown>,
|
|
41
|
+
embeddingModel: string,
|
|
42
|
+
content: ContentType[],
|
|
43
|
+
): boolean {
|
|
44
|
+
try {
|
|
45
|
+
const stored = metadata.content_type as ContentType[];
|
|
46
|
+
const model = metadata.embedding_model as string;
|
|
47
|
+
if (model !== embeddingModel) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const expectedDims = resolveEmbeddingDimensions(embeddingModel);
|
|
51
|
+
const storedDims = metadata.embedding_dimensions as number | undefined;
|
|
52
|
+
if (expectedDims != null && storedDims !== expectedDims) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (semanticStorageFromMetadata(metadata) !== resolveSemanticStorage()) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const a = new Set(stored);
|
|
59
|
+
const b = new Set(content);
|
|
60
|
+
if (a.size !== b.size) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
for (const c of a) {
|
|
64
|
+
if (!b.has(c)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function getValidatedCache(
|
|
75
|
+
path: string,
|
|
76
|
+
embeddingModel: string | undefined,
|
|
77
|
+
content: ContentType[],
|
|
78
|
+
): Promise<string | null> {
|
|
79
|
+
const indexPath = findIndexCachePath(path);
|
|
80
|
+
const paths = persistencePaths(indexPath);
|
|
81
|
+
const missing = await pathsExist(paths);
|
|
82
|
+
if (missing.length > 0) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const model = embeddingModel ?? resolveEmbeddingModel();
|
|
87
|
+
const metadata = JSON.parse(await Bun.file(paths.metadata).text()) as Record<string, unknown>;
|
|
88
|
+
if (!metadataMatches(metadata, model, content)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const semanticMeta = JSON.parse(await Bun.file(`${paths.semanticIndex}/meta.json`).text()) as {
|
|
93
|
+
dimensions: number;
|
|
94
|
+
};
|
|
95
|
+
const expectedDims = resolveEmbeddingDimensions(model);
|
|
96
|
+
if (expectedDims != null && semanticMeta.dimensions !== expectedDims) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (!(await semanticIndexMatchesStorage(paths.semanticIndex))) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rootPath = metadata.root_path as string | null;
|
|
104
|
+
if (rootPath) {
|
|
105
|
+
const filePaths = metadata.file_paths as string[];
|
|
106
|
+
try {
|
|
107
|
+
await Bun.file(rootPath).stat();
|
|
108
|
+
for (const rel of filePaths) {
|
|
109
|
+
try {
|
|
110
|
+
await Bun.file(join(rootPath, rel)).stat();
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return indexPath;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function loadCachedIndex(indexPath: string): Promise<{
|
|
124
|
+
bm25: Awaited<ReturnType<typeof loadBm25>>;
|
|
125
|
+
semantic: Awaited<ReturnType<typeof loadSemantic>>;
|
|
126
|
+
chunks: Chunk[];
|
|
127
|
+
metadata: Record<string, unknown>;
|
|
128
|
+
}> {
|
|
129
|
+
const paths = persistencePaths(indexPath);
|
|
130
|
+
const bm25 = await loadBm25(paths.bm25Index);
|
|
131
|
+
const semantic = await loadSemantic(paths.semanticIndex);
|
|
132
|
+
const chunkData = JSON.parse(await Bun.file(paths.chunks).text()) as Record<string, unknown>[];
|
|
133
|
+
const chunks = chunkData.map(chunkFromDict);
|
|
134
|
+
const metadata = JSON.parse(await Bun.file(paths.metadata).text()) as Record<string, unknown>;
|
|
135
|
+
return { bm25, semantic, chunks, metadata };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function clearCache(path: string): Promise<void> {
|
|
139
|
+
const indexPath = findIndexCachePath(path);
|
|
140
|
+
await rm(indexPath, { recursive: true, force: true });
|
|
141
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Chunk } from "../types.ts";
|
|
2
|
+
import { chunkLines } from "./lines.ts";
|
|
3
|
+
import { chunkStructural } from "./structural.ts";
|
|
4
|
+
|
|
5
|
+
/** Match upstream hybrid-search chunking target length. */
|
|
6
|
+
const DESIRED_CHUNK_LENGTH_CHARS = 1500;
|
|
7
|
+
|
|
8
|
+
export function chunkSource(source: string, filePath: string, language: string | null): Chunk[] {
|
|
9
|
+
if (!source.trim()) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const boundaries =
|
|
14
|
+
chunkStructural(source, language, DESIRED_CHUNK_LENGTH_CHARS) ??
|
|
15
|
+
chunkLines(source, DESIRED_CHUNK_LENGTH_CHARS);
|
|
16
|
+
const chunks: Chunk[] = [];
|
|
17
|
+
const prefixNewlineCounts = new Uint32Array(source.length + 1);
|
|
18
|
+
for (let i = 0; i < source.length; i++) {
|
|
19
|
+
const prevCount = prefixNewlineCounts[i] ?? 0;
|
|
20
|
+
prefixNewlineCounts[i + 1] = prevCount + (source[i] === "\n" ? 1 : 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const boundary of boundaries) {
|
|
24
|
+
const endIndex = Math.max(boundary.end - 1, boundary.start);
|
|
25
|
+
const text = source.slice(boundary.start, endIndex + 1);
|
|
26
|
+
chunks.push({
|
|
27
|
+
content: text,
|
|
28
|
+
file_path: filePath,
|
|
29
|
+
start_line: (prefixNewlineCounts[boundary.start] ?? 0) + 1,
|
|
30
|
+
end_line: (prefixNewlineCounts[endIndex] ?? 0) + 1,
|
|
31
|
+
language,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return chunks;
|
|
36
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export interface ChunkBoundary {
|
|
2
|
+
start: number;
|
|
3
|
+
end: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface LineGroup extends ChunkBoundary {
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function mergeAdjacentChunks(
|
|
11
|
+
chunks: ChunkBoundary[],
|
|
12
|
+
desiredLength: number,
|
|
13
|
+
): ChunkBoundary[] {
|
|
14
|
+
if (chunks.length === 0) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const first = chunks[0];
|
|
19
|
+
if (!first) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const merged: ChunkBoundary[] = [];
|
|
24
|
+
let currentStart = first.start;
|
|
25
|
+
let currentEnd = first.end;
|
|
26
|
+
let currentLength = currentEnd - currentStart;
|
|
27
|
+
|
|
28
|
+
for (const group of chunks.slice(1)) {
|
|
29
|
+
const start = group.start;
|
|
30
|
+
const end = group.end;
|
|
31
|
+
const length = end - start;
|
|
32
|
+
|
|
33
|
+
if (currentLength + length > desiredLength) {
|
|
34
|
+
merged.push({ start: currentStart, end: currentEnd });
|
|
35
|
+
currentStart = start;
|
|
36
|
+
currentEnd = end;
|
|
37
|
+
currentLength = length;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
currentEnd = end;
|
|
42
|
+
currentLength += length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
merged.push({ start: currentStart, end: currentEnd });
|
|
46
|
+
return merged;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function splitLinesKeepEnds(source: string): LineGroup[] {
|
|
50
|
+
const groups: LineGroup[] = [];
|
|
51
|
+
const re = /.*(?:\r\n|\n|\r|$)/g;
|
|
52
|
+
let match = re.exec(source);
|
|
53
|
+
while (match !== null) {
|
|
54
|
+
const text = match[0] ?? "";
|
|
55
|
+
if (text.length === 0) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
const start = match.index;
|
|
59
|
+
const end = start + text.length;
|
|
60
|
+
groups.push({ start, end, text });
|
|
61
|
+
if (re.lastIndex >= source.length) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
match = re.exec(source);
|
|
65
|
+
}
|
|
66
|
+
return groups;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function chunkLines(source: string, desiredLength: number): ChunkBoundary[] {
|
|
70
|
+
if (!source.trim()) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const lineGroups = splitLinesKeepEnds(source).map(({ start, end }) => ({
|
|
75
|
+
start,
|
|
76
|
+
end,
|
|
77
|
+
}));
|
|
78
|
+
return mergeAdjacentChunks(lineGroups, desiredLength);
|
|
79
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ChunkBoundary,
|
|
3
|
+
type LineGroup,
|
|
4
|
+
mergeAdjacentChunks,
|
|
5
|
+
splitLinesKeepEnds,
|
|
6
|
+
} from "./lines.ts";
|
|
7
|
+
|
|
8
|
+
const SUPPORTED_STRUCTURAL_LANGUAGES = new Set(["python", "go", "typescript", "javascript"]);
|
|
9
|
+
|
|
10
|
+
export function chunkStructural(
|
|
11
|
+
source: string,
|
|
12
|
+
language: string | null,
|
|
13
|
+
desiredLength: number,
|
|
14
|
+
): ChunkBoundary[] | null {
|
|
15
|
+
if (!language || !SUPPORTED_STRUCTURAL_LANGUAGES.has(language)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const lines = splitLinesKeepEnds(source);
|
|
20
|
+
if (lines.length === 0) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let units: ChunkBoundary[];
|
|
25
|
+
if (language === "python") {
|
|
26
|
+
units = pythonUnits(lines);
|
|
27
|
+
} else {
|
|
28
|
+
units = braceUnits(lines, language);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (units.length === 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return mergeAdjacentChunks(units, desiredLength);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function lineIndent(text: string): number {
|
|
38
|
+
let i = 0;
|
|
39
|
+
while (i < text.length && (text[i] === " " || text[i] === "\t")) {
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
return i;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function stripLine(text: string): string {
|
|
46
|
+
return text.replace(/[\r\n]+$/, "");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function _isBlank(text: string): boolean {
|
|
50
|
+
return stripLine(text).trim().length === 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function _isCommentLine(text: string, language: string): boolean {
|
|
54
|
+
const trimmed = stripLine(text).trim();
|
|
55
|
+
if (language === "python") {
|
|
56
|
+
return trimmed.startsWith("#");
|
|
57
|
+
}
|
|
58
|
+
return trimmed.startsWith("//");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pythonUnits(lines: LineGroup[]): ChunkBoundary[] {
|
|
62
|
+
const units: ChunkBoundary[] = [];
|
|
63
|
+
let firstDeclStart: number | null = null;
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < lines.length; i++) {
|
|
66
|
+
const raw = stripLine(lines[i]?.text);
|
|
67
|
+
const trimmed = raw.trim();
|
|
68
|
+
if (!trimmed.match(/^(async\s+def|def|class)\b/)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (firstDeclStart === null) {
|
|
72
|
+
firstDeclStart = lines[i]?.start;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const declIndent = lineIndent(raw);
|
|
76
|
+
|
|
77
|
+
if (trimmed.startsWith("class ")) {
|
|
78
|
+
const classEndLine = findPythonBlockEnd(lines, i, declIndent);
|
|
79
|
+
const methodStarts: number[] = [];
|
|
80
|
+
for (let j = i + 1; j <= classEndLine; j++) {
|
|
81
|
+
const innerRaw = stripLine(lines[j]?.text);
|
|
82
|
+
const innerTrim = innerRaw.trim();
|
|
83
|
+
if (!innerTrim.match(/^(async\s+def|def)\b/)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (lineIndent(innerRaw) > declIndent) {
|
|
87
|
+
methodStarts.push(j);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (methodStarts.length > 0) {
|
|
92
|
+
const firstMethodStart = methodStarts[0];
|
|
93
|
+
if (firstMethodStart === undefined) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const firstMethodEnd = findPythonBlockEnd(
|
|
97
|
+
lines,
|
|
98
|
+
firstMethodStart,
|
|
99
|
+
lineIndent(stripLine(lines[firstMethodStart]?.text)),
|
|
100
|
+
);
|
|
101
|
+
units.push({ start: lines[i]?.start, end: lines[firstMethodEnd]?.end });
|
|
102
|
+
for (let m = 1; m < methodStarts.length; m++) {
|
|
103
|
+
const methodStart = methodStarts[m];
|
|
104
|
+
if (methodStart === undefined) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const methodIndent = lineIndent(stripLine(lines[methodStart]?.text));
|
|
108
|
+
const methodEnd = findPythonBlockEnd(lines, methodStart, methodIndent);
|
|
109
|
+
units.push({ start: lines[methodStart]?.start, end: lines[methodEnd]?.end });
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
units.push({ start: lines[i]?.start, end: lines[classEndLine]?.end });
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let startLine = i;
|
|
118
|
+
while (startLine - 1 >= 0) {
|
|
119
|
+
const prev = stripLine(lines[startLine - 1]?.text).trim();
|
|
120
|
+
if (prev.startsWith("@")) {
|
|
121
|
+
startLine--;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const endLine = findPythonBlockEnd(lines, i, declIndent);
|
|
128
|
+
|
|
129
|
+
// Keep top-level defs and class blocks; methods are represented by their class block.
|
|
130
|
+
if (declIndent > 0) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const start = lines[startLine]?.start;
|
|
135
|
+
const end = lines[endLine]?.end;
|
|
136
|
+
if (end > start) {
|
|
137
|
+
units.push({ start, end });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (firstDeclStart !== null && firstDeclStart > 0) {
|
|
142
|
+
units.unshift({ start: 0, end: firstDeclStart });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return dedupeAndSort(units);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function findPythonBlockEnd(lines: LineGroup[], start: number, indent: number): number {
|
|
149
|
+
let endLine = lines.length - 1;
|
|
150
|
+
for (let j = start + 1; j < lines.length; j++) {
|
|
151
|
+
const nextRaw = stripLine(lines[j]?.text);
|
|
152
|
+
if (!nextRaw.trim() || nextRaw.trim().startsWith("#")) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (lineIndent(nextRaw) <= indent) {
|
|
156
|
+
endLine = j - 1;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return endLine;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function declPattern(language: string): RegExp {
|
|
164
|
+
if (language === "go") {
|
|
165
|
+
return /^\s*(func\b|type\b.*\b(struct|interface)\b)/;
|
|
166
|
+
}
|
|
167
|
+
return /^\s*(export\s+)?(async\s+function\b|function\b|class\b|interface\b|type\b|(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)(?:\s*:\s*[^=]+)?\s*=>)/;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function braceUnits(lines: LineGroup[], language: string): ChunkBoundary[] {
|
|
171
|
+
const units: ChunkBoundary[] = [];
|
|
172
|
+
const full = lines.map((l) => l.text).join("");
|
|
173
|
+
const re = declPattern(language);
|
|
174
|
+
let firstDeclStart: number | null = null;
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < lines.length; i++) {
|
|
177
|
+
const text = stripLine(lines[i]?.text);
|
|
178
|
+
if (!re.test(text)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (firstDeclStart === null) {
|
|
182
|
+
firstDeclStart = lines[i]?.start;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const startOffset = lines[i]?.start;
|
|
186
|
+
const openPos = full.indexOf("{", startOffset);
|
|
187
|
+
if (openPos === -1) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const limit = i + 60 < lines.length ? lines[i + 60]?.end : full.length;
|
|
192
|
+
if (openPos >= limit) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let depth = 0;
|
|
197
|
+
let endPos = -1;
|
|
198
|
+
for (let p = openPos; p < full.length; p++) {
|
|
199
|
+
const ch = full[p];
|
|
200
|
+
if (ch === "{") {
|
|
201
|
+
depth++;
|
|
202
|
+
} else if (ch === "}") {
|
|
203
|
+
depth--;
|
|
204
|
+
if (depth === 0) {
|
|
205
|
+
endPos = p + 1;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (endPos > startOffset) {
|
|
211
|
+
let extendedEnd = endPos;
|
|
212
|
+
while (
|
|
213
|
+
extendedEnd < full.length &&
|
|
214
|
+
(full[extendedEnd] === ";" || full[extendedEnd] === " " || full[extendedEnd] === "\t")
|
|
215
|
+
) {
|
|
216
|
+
extendedEnd++;
|
|
217
|
+
}
|
|
218
|
+
if (extendedEnd < full.length && full[extendedEnd] === "\n") {
|
|
219
|
+
extendedEnd++;
|
|
220
|
+
}
|
|
221
|
+
units.push({ start: startOffset, end: extendedEnd });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (firstDeclStart !== null && firstDeclStart > 0) {
|
|
226
|
+
units.unshift({ start: 0, end: firstDeclStart });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return dedupeAndSort(units);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function dedupeAndSort(units: ChunkBoundary[]): ChunkBoundary[] {
|
|
233
|
+
const out: ChunkBoundary[] = [];
|
|
234
|
+
const seen = new Set<string>();
|
|
235
|
+
for (const u of units.sort((a, b) => a.start - b.start || a.end - b.end)) {
|
|
236
|
+
const key = `${u.start}:${u.end}`;
|
|
237
|
+
if (seen.has(key)) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
seen.add(key);
|
|
241
|
+
out.push(u);
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|