Skip to content

Commit db9c850

Browse files
authored
chore: refactor toml parsing (#245)
1 parent 73a67ed commit db9c850

7 files changed

Lines changed: 332 additions & 675 deletions

File tree

src/providers/inlineToolDecorator.ts

Lines changed: 32 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,29 @@ import type { MiseService } from "../miseService";
99
import { expandPath } from "../utils/fileUtils";
1010
import { getSvgIcon } from "../utils/iconUtils";
1111
import { logger } from "../utils/logger";
12+
import { getCachedTomlParser } from "../utils/miseFileParser";
1213
import { getCleanedToolName } from "../utils/miseUtilts";
13-
import {
14-
extractToolNamesFromLine,
15-
extractToolVersionFromLine,
16-
extractToolVersionFromSection,
17-
isPositionInToolsContext,
18-
parseToolsSectionHeader,
19-
} from "../utils/tomlParsing";
14+
import { buildToolIndex, type DeclaredTool } from "../utils/toolIndex";
15+
16+
function groupToolsByLine(
17+
document: vscode.TextDocument,
18+
): Map<number, DeclaredTool[]> {
19+
const toolsByLine = new Map<number, DeclaredTool[]>();
20+
const parser = getCachedTomlParser(document);
21+
if (!parser) {
22+
return toolsByLine;
23+
}
24+
for (const tool of buildToolIndex(parser)) {
25+
const line = tool.range.start.line;
26+
const existing = toolsByLine.get(line);
27+
if (existing) {
28+
existing.push(tool);
29+
} else {
30+
toolsByLine.set(line, [tool]);
31+
}
32+
}
33+
return toolsByLine;
34+
}
2035

2136
const activeDecorationsPerFileAndTool: {
2237
[filePath: string]: {
@@ -69,34 +84,17 @@ export async function showToolVersionInline(
6984
// (e.g. `pkl` in [tools] AND `tools.pkl` in a task) would lose the first.
7085
const pendingDecorations = new Map<string, vscode.DecorationOptions[]>();
7186

72-
for (let line = 0; line < document.lineCount; line++) {
87+
for (const [line, lineTools] of groupToolsByLine(document)) {
7388
try {
7489
const lineText = document.lineAt(line).text;
75-
const trimmedLine = lineText.trim();
76-
77-
const { inContext, isInline, inToolOptionsSection } =
78-
isPositionInToolsContext(document, new vscode.Position(line, 0));
79-
// Option lines inside a `[tools.<name>]` section (version, os, ...)
80-
// do not declare tools; the section header line does.
81-
if (!inContext || inToolOptionsSection) {
82-
continue;
83-
}
84-
85-
if (trimmedLine.startsWith("#") || trimmedLine === "[tools]") {
86-
continue;
87-
}
88-
89-
const toolNamesRaw = extractToolNamesFromLine(lineText);
90-
if (toolNamesRaw.length === 0) {
91-
continue;
92-
}
93-
94-
const isToolsSectionHeader = !!parseToolsSectionHeader(trimmedLine);
90+
// task tools (`tools = { ... }`, `tools.<name> = ...`) get the
91+
// `name: version` annotation style; config tools show the version only
92+
const isInline = lineTools.some((tool) => tool.inTask);
9593
const annotations: string[] = [];
9694
const usedTools: string[] = [];
9795

98-
for (const raw of toolNamesRaw) {
99-
const cleanedToolName = getCleanedToolName(raw);
96+
for (const declaredTool of lineTools) {
97+
const cleanedToolName = getCleanedToolName(declaredTool.toolName);
10098
if (!cleanedToolName) {
10199
continue;
102100
}
@@ -126,11 +124,7 @@ export async function showToolVersionInline(
126124
}
127125
}
128126

129-
const reqVersion =
130-
extractToolVersionFromLine(lineText, raw) ??
131-
(isToolsSectionHeader
132-
? extractToolVersionFromSection(document, line)
133-
: undefined);
127+
const reqVersion = declaredTool.requestedVersion;
134128
if (reqVersion && resolvedVersion) {
135129
// Strip a leading `v` from both sides so git-sourced tools
136130
// (e.g. `pipx:github/owner/repo` pinned to a tag like
@@ -258,32 +252,14 @@ export async function showOutdatedToolsGutterIcons(
258252
const updatedToolNames = new Set<string>();
259253
const linesWithOutdatedTools: number[] = [];
260254

261-
for (let line = 0; line < document.lineCount; line++) {
255+
for (const [line, lineTools] of groupToolsByLine(document)) {
262256
try {
263-
const lineText = document.lineAt(line).text;
264-
const trimmedLine = lineText.trim();
265-
266-
const { inContext, inToolOptionsSection } = isPositionInToolsContext(
267-
document,
268-
new vscode.Position(line, 0),
269-
);
270-
if (!inContext || inToolOptionsSection) continue;
271-
272-
if (trimmedLine.startsWith("#") || trimmedLine === "[tools]") {
273-
continue;
274-
}
275-
276-
const toolNamesRaw = extractToolNamesFromLine(lineText);
277-
if (toolNamesRaw.length === 0) {
278-
continue;
279-
}
280-
281257
let hasOutdated = false;
282258
const outdatedNames: string[] = [];
283259
const validToolNames: string[] = [];
284260

285-
for (const raw of toolNamesRaw) {
286-
const cleanedToolName = getCleanedToolName(raw);
261+
for (const declaredTool of lineTools) {
262+
const cleanedToolName = getCleanedToolName(declaredTool.toolName);
287263
if (!cleanedToolName) {
288264
continue;
289265
}

src/providers/toolHoverProvider.ts

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,9 @@ import type { DocumentSelector } from "vscode";
22
import vscode from "vscode";
33
import { isMiseExtensionEnabled } from "../configuration";
44
import type { MiseService } from "../miseService";
5+
import { getCachedTomlParser } from "../utils/miseFileParser";
56
import { getCleanedToolName, getWebsiteForTool } from "../utils/miseUtilts";
6-
import {
7-
extractToolNamesFromLine,
8-
extractToolVersionFromLine,
9-
extractToolVersionFromSection,
10-
isPositionInToolsContext,
11-
parseToolsSectionHeader,
12-
} from "../utils/tomlParsing";
7+
import { buildToolIndex } from "../utils/toolIndex";
138

149
export const createToolHoverProvider = (
1510
documentSelector: DocumentSelector,
@@ -24,55 +19,33 @@ export const createToolHoverProvider = (
2419
return;
2520
}
2621

27-
const wordRange = document.getWordRangeAtPosition(position);
28-
if (!wordRange) {
22+
const parser = getCachedTomlParser(document);
23+
if (!parser) {
2924
return;
3025
}
3126

32-
const { inContext, inToolOptionsSection } = isPositionInToolsContext(
33-
document,
34-
position,
27+
const tool = buildToolIndex(parser).find((t) =>
28+
t.range.contains(position),
3529
);
36-
if (!inContext || inToolOptionsSection) {
30+
if (!tool) {
3731
return;
3832
}
3933

40-
const word = document.getText(wordRange);
41-
const toolNamesRaw = extractToolNamesFromLine(
42-
document.lineAt(position).text,
43-
word,
44-
);
45-
if (toolNamesRaw.length === 0) {
46-
return;
47-
}
48-
const toolNameRaw = toolNamesRaw[0];
49-
if (!toolNameRaw) {
50-
return;
51-
}
52-
53-
const toolName = getCleanedToolName(toolNameRaw);
34+
const toolName = getCleanedToolName(tool.toolName);
5435
if (!toolName) {
5536
return;
5637
}
5738

58-
const toolInfo = await miseService.miseToolInfo(toolNameRaw);
39+
const toolInfo = await miseService.miseToolInfo(tool.toolName);
5940
if (!toolInfo) {
6041
return;
6142
}
6243

6344
const markdownString = new vscode.MarkdownString("");
6445
const toolWebsite = await getWebsiteForTool(toolInfo);
6546

66-
const lineText = document.lineAt(position).text;
67-
const parsedRequestedVersion =
68-
extractToolVersionFromLine(lineText, toolNameRaw) ??
69-
// `[tools.<name>]` header: the version is on a line inside the section
70-
(parseToolsSectionHeader(lineText)
71-
? extractToolVersionFromSection(document, position.line)
72-
: undefined);
7347
const displayRequestedVersion =
74-
parsedRequestedVersion ||
75-
(toolInfo.requested_versions ?? []).join(", ");
48+
tool.requestedVersion || (toolInfo.requested_versions ?? []).join(", ");
7649

7750
markdownString.appendMarkdown(
7851
[
@@ -105,6 +78,6 @@ export const createToolHoverProvider = (
10578
.join("\n\n"),
10679
);
10780

108-
return new vscode.Hover([markdownString]);
81+
return new vscode.Hover([markdownString], tool.range);
10982
},
11083
});

src/providers/toolLinkProvider.ts

Lines changed: 24 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@ import type { DocumentSelector } from "vscode";
22
import vscode from "vscode";
33
import { isMiseExtensionEnabled, isToolLinksEnabled } from "../configuration";
44
import type { MiseService } from "../miseService";
5+
import { getCachedTomlParser } from "../utils/miseFileParser";
56
import {
67
getCleanedToolName,
78
getWebsiteForTool,
89
getWebsiteFromToolName,
910
} from "../utils/miseUtilts";
10-
import {
11-
extractToolNamesFromLine,
12-
isPositionInToolsContext,
13-
} from "../utils/tomlParsing";
11+
import { buildToolIndex } from "../utils/toolIndex";
1412

1513
async function resolveToolLink(
1614
miseService: MiseService,
@@ -50,84 +48,36 @@ export const createToolLinkProvider = (
5048
return [];
5149
}
5250

51+
const parser = getCachedTomlParser(document);
52+
if (!parser) {
53+
return [];
54+
}
55+
5356
const links: vscode.DocumentLink[] = [];
5457
const linkPromises: Promise<void>[] = [];
5558

56-
for (let i = 0; i < document.lineCount; i++) {
57-
const line = document.lineAt(i);
58-
const text = line.text.trim();
59-
60-
const { inContext, inToolOptionsSection } = isPositionInToolsContext(
61-
document,
62-
new vscode.Position(i, 0),
63-
);
64-
if (
65-
!inContext ||
66-
inToolOptionsSection ||
67-
text.length === 0 ||
68-
text.startsWith("#")
69-
) {
59+
for (const { toolName, range } of buildToolIndex(parser)) {
60+
const cleanedToolName = getCleanedToolName(toolName);
61+
if (!cleanedToolName) {
7062
continue;
7163
}
7264

73-
const toolNames = extractToolNamesFromLine(line.text);
74-
75-
// On `[tools.<name>]` headers, search for the tool name after the
76-
// `tools.` prefix so a tool like `tool` does not match inside `tools`
77-
const headerPrefixIndex = line.text.indexOf("[tools.");
78-
const searchFrom =
79-
headerPrefixIndex === -1 ? 0 : headerPrefixIndex + "[tools.".length;
80-
81-
for (const toolName of toolNames) {
82-
if (!toolName) continue;
83-
84-
const cleanedToolName = getCleanedToolName(toolName);
85-
if (!cleanedToolName) continue;
86-
87-
const quotedDouble = `"${toolName}"`;
88-
const quotedSingle = `'${toolName}'`;
89-
let startIndex: number;
90-
let endIndex: number;
91-
if (line.text.includes(quotedDouble)) {
92-
startIndex = line.text.indexOf(quotedDouble, searchFrom);
93-
endIndex = startIndex + quotedDouble.length;
94-
} else if (line.text.includes(quotedSingle)) {
95-
startIndex = line.text.indexOf(quotedSingle, searchFrom);
96-
endIndex = startIndex + quotedSingle.length;
97-
} else {
98-
startIndex = line.text.indexOf(toolName, searchFrom);
99-
endIndex = startIndex + toolName.length;
100-
}
101-
102-
if (startIndex === -1) {
103-
continue;
104-
}
105-
106-
const toolWebsite = getWebsiteFromToolName(cleanedToolName);
107-
if (toolWebsite) {
108-
try {
109-
const range = new vscode.Range(
110-
new vscode.Position(i, startIndex),
111-
new vscode.Position(i, endIndex),
112-
);
113-
links.push(
114-
new vscode.DocumentLink(range, vscode.Uri.parse(toolWebsite)),
115-
);
116-
} catch {
117-
// ignore invalid URI
118-
}
119-
} else {
120-
// Slow path: call miseToolInfo for backends that need tool_options
121-
const range = new vscode.Range(
122-
new vscode.Position(i, startIndex),
123-
new vscode.Position(i, endIndex),
124-
);
125-
linkPromises.push(
126-
resolveToolLink(miseService, toolName, range, links).catch(
127-
() => {}, // Ignore errors for individual tools
128-
),
65+
const toolWebsite = getWebsiteFromToolName(cleanedToolName);
66+
if (toolWebsite) {
67+
try {
68+
links.push(
69+
new vscode.DocumentLink(range, vscode.Uri.parse(toolWebsite)),
12970
);
71+
} catch {
72+
// ignore invalid URI
13073
}
74+
} else {
75+
// Slow path: call miseToolInfo for backends that need tool_options
76+
linkPromises.push(
77+
resolveToolLink(miseService, toolName, range, links).catch(
78+
() => {}, // Ignore errors for individual tools
79+
),
80+
);
13181
}
13282
}
13383

0 commit comments

Comments
 (0)