f044f3ba75
- Added language-specific import management for TypeScript, JavaScript, Python, Rust, Go, and C/C++ with automatic deduplication and grouping. - Added `imports` parameter to patch, hashline, and replace edit operations for structured import/include requests with named, default, namespace, and alias support. - Added `edit.manageImports` setting to control post-edit import management behavior across all edit operations. - Implemented language-specific import handlers with parsing, merging, and rendering logic for each supported language. - Integrated import application into EditTool with feature flag support and comprehensive test coverage across all languages.
654 lines
20 KiB
TypeScript
654 lines
20 KiB
TypeScript
import type { ImportSpec } from "../types";
|
|
import {
|
|
defaultImportStyle,
|
|
type ImportHandler,
|
|
type ImportRegion,
|
|
type ImportStyle,
|
|
type ParsedImport,
|
|
} from "./types";
|
|
|
|
type ImportGroup = "stdlib" | "thirdparty" | "local";
|
|
|
|
interface PythonImportStyle extends ImportStyle {
|
|
parenthesized: boolean;
|
|
}
|
|
|
|
interface ImportedName {
|
|
name: string;
|
|
alias?: string;
|
|
order: number;
|
|
}
|
|
|
|
interface ParsedPythonImport extends ParsedImport {
|
|
kind: "module" | "from";
|
|
group: ImportGroup;
|
|
order: number;
|
|
multiline: boolean;
|
|
startLine: number;
|
|
endLine: number;
|
|
names: string[];
|
|
parsedNames: ImportedName[];
|
|
}
|
|
|
|
interface ParsedPythonRegion extends ImportRegion {
|
|
imports: ParsedPythonImport[];
|
|
style: PythonImportStyle;
|
|
}
|
|
|
|
const DEFAULT_GROUP_ORDER: ImportGroup[] = ["stdlib", "thirdparty", "local"];
|
|
|
|
const STDLIB_MODULES = new Set<string>([
|
|
"__future__",
|
|
"abc",
|
|
"argparse",
|
|
"array",
|
|
"ast",
|
|
"asyncio",
|
|
"base64",
|
|
"collections",
|
|
"contextlib",
|
|
"copy",
|
|
"csv",
|
|
"dataclasses",
|
|
"datetime",
|
|
"decimal",
|
|
"enum",
|
|
"functools",
|
|
"glob",
|
|
"gzip",
|
|
"hashlib",
|
|
"heapq",
|
|
"hmac",
|
|
"html",
|
|
"http",
|
|
"importlib",
|
|
"inspect",
|
|
"io",
|
|
"itertools",
|
|
"json",
|
|
"logging",
|
|
"math",
|
|
"mimetypes",
|
|
"os",
|
|
"pathlib",
|
|
"pickle",
|
|
"platform",
|
|
"queue",
|
|
"random",
|
|
"re",
|
|
"secrets",
|
|
"shlex",
|
|
"shutil",
|
|
"socket",
|
|
"sqlite3",
|
|
"statistics",
|
|
"string",
|
|
"subprocess",
|
|
"sys",
|
|
"tempfile",
|
|
"threading",
|
|
"time",
|
|
"traceback",
|
|
"types",
|
|
"typing",
|
|
"unittest",
|
|
"urllib",
|
|
"uuid",
|
|
"warnings",
|
|
"weakref",
|
|
"xml",
|
|
"zipfile",
|
|
"zoneinfo",
|
|
]);
|
|
|
|
function getPythonStyle(style: ImportStyle): PythonImportStyle {
|
|
return {
|
|
...style,
|
|
parenthesized: (style as Partial<PythonImportStyle>).parenthesized ?? false,
|
|
};
|
|
}
|
|
|
|
function detectEol(content: string): string {
|
|
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
}
|
|
|
|
function isBlankLine(line: string): boolean {
|
|
return line.trim().length === 0;
|
|
}
|
|
|
|
function isCommentLine(line: string): boolean {
|
|
return line.trimStart().startsWith("#");
|
|
}
|
|
|
|
function isEncodingComment(line: string): boolean {
|
|
return /^[ \t]*#.*coding[:=][ \t]*[-\w.]+/.test(line);
|
|
}
|
|
|
|
function isImportStart(line: string): boolean {
|
|
const trimmed = line.trimStart();
|
|
return trimmed.startsWith("import ") || trimmed.startsWith("from ");
|
|
}
|
|
|
|
function stripInlineComment(line: string): string {
|
|
const hashIndex = line.indexOf("#");
|
|
return hashIndex === -1 ? line : line.slice(0, hashIndex);
|
|
}
|
|
|
|
function parseModuleDocstring(lines: string[], startIndex: number): number | null {
|
|
const firstLine = lines[startIndex];
|
|
if (firstLine === undefined) return null;
|
|
const trimmed = firstLine.trimStart();
|
|
const match = /^[rRuUbBfF]*(["']{3})/.exec(trimmed);
|
|
if (!match) return null;
|
|
const delimiter = match[1];
|
|
const remainder = trimmed.slice(match[0].length);
|
|
if (remainder.includes(delimiter)) return startIndex + 1;
|
|
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
|
if (lines[index]?.includes(delimiter)) return index + 1;
|
|
}
|
|
return lines.length;
|
|
}
|
|
|
|
function findPrefixEnd(lines: string[]): number {
|
|
let index = 0;
|
|
if (lines[0]?.startsWith("#!")) index += 1;
|
|
if (isEncodingComment(lines[index] ?? "")) index += 1;
|
|
while (index < lines.length && isBlankLine(lines[index] ?? "")) index += 1;
|
|
const docstringEnd = parseModuleDocstring(lines, index);
|
|
if (docstringEnd !== null) {
|
|
index = docstringEnd;
|
|
while (index < lines.length && isBlankLine(lines[index] ?? "")) index += 1;
|
|
}
|
|
return index;
|
|
}
|
|
|
|
function classifyGroup(source: string): ImportGroup {
|
|
if (source.startsWith(".")) return "local";
|
|
const root = source.split(".")[0] ?? source;
|
|
return STDLIB_MODULES.has(root) ? "stdlib" : "thirdparty";
|
|
}
|
|
|
|
function formatImportedName(name: ImportedName): string {
|
|
return name.alias ? `${name.name} as ${name.alias}` : name.name;
|
|
}
|
|
|
|
function parseImportedName(token: string, order: number): ImportedName | null {
|
|
const cleaned = stripInlineComment(token).replace(/,$/, "").trim();
|
|
if (cleaned.length === 0) return null;
|
|
const match = /^(\*|[A-Za-z_][\w]*)(?:\s+as\s+([A-Za-z_][\w]*))?$/.exec(cleaned);
|
|
if (!match) return null;
|
|
return { name: match[1], alias: match[2], order };
|
|
}
|
|
|
|
function parseModuleImport(token: string, order: number): ParsedPythonImport | null {
|
|
const cleaned = stripInlineComment(token).replace(/,$/, "").trim();
|
|
if (cleaned.length === 0) return null;
|
|
const match = /^([A-Za-z_][\w.]*)(?:\s+as\s+([A-Za-z_][\w]*))?$/.exec(cleaned);
|
|
if (!match) return null;
|
|
return {
|
|
raw: `import ${cleaned}`,
|
|
source: match[1],
|
|
names: [],
|
|
parsedNames: [],
|
|
alias: match[2],
|
|
kind: "module",
|
|
group: classifyGroup(match[1]),
|
|
order,
|
|
multiline: false,
|
|
startLine: 0,
|
|
endLine: 0,
|
|
};
|
|
}
|
|
|
|
function parseImportStatement(
|
|
line: string,
|
|
lineNumber: number,
|
|
startOrder: number,
|
|
): { records: ParsedPythonImport[]; nextOrder: number } | null {
|
|
const match = /^import\s+(.+)$/.exec(stripInlineComment(line.trim()));
|
|
if (!match) return null;
|
|
let nextOrder = startOrder;
|
|
const records: ParsedPythonImport[] = [];
|
|
for (const token of match[1].split(",")) {
|
|
const parsed = parseModuleImport(token, nextOrder);
|
|
if (!parsed) continue;
|
|
parsed.startLine = lineNumber;
|
|
parsed.endLine = lineNumber;
|
|
parsed.raw = line;
|
|
records.push(parsed);
|
|
nextOrder += 1;
|
|
}
|
|
return records.length === 0 ? null : { records, nextOrder };
|
|
}
|
|
|
|
function countParentheses(text: string): number {
|
|
let balance = 0;
|
|
for (const char of text) {
|
|
if (char === "(") balance += 1;
|
|
if (char === ")") balance -= 1;
|
|
}
|
|
return balance;
|
|
}
|
|
|
|
function parseFromStatement(
|
|
lines: string[],
|
|
startIndex: number,
|
|
eol: string,
|
|
startOrder: number,
|
|
): { record: ParsedPythonImport; nextIndex: number; nextOrder: number } | null {
|
|
const collected: string[] = [];
|
|
let nextIndex = startIndex;
|
|
let balance = 0;
|
|
let sawParenthesis = false;
|
|
while (nextIndex < lines.length) {
|
|
const line = lines[nextIndex] ?? "";
|
|
collected.push(line);
|
|
const withoutComment = stripInlineComment(line);
|
|
balance += countParentheses(withoutComment);
|
|
sawParenthesis ||= withoutComment.includes("(");
|
|
nextIndex += 1;
|
|
if (sawParenthesis) {
|
|
if (balance <= 0) break;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
const normalized = collected
|
|
.map(line => stripInlineComment(line).trim())
|
|
.join(" ")
|
|
.replace(/\s+/g, " ");
|
|
const match = /^from\s+([.A-Za-z_][\w.]*)\s+import\s+(.+)$/.exec(normalized);
|
|
if (!match) return null;
|
|
let namesPart = match[2].trim();
|
|
if (namesPart.startsWith("(") && namesPart.endsWith(")")) {
|
|
namesPart = namesPart.slice(1, -1).trim();
|
|
}
|
|
const parsedNames: ImportedName[] = [];
|
|
let nextOrder = startOrder;
|
|
for (const token of namesPart.split(",")) {
|
|
const parsedName = parseImportedName(token, nextOrder);
|
|
if (!parsedName) continue;
|
|
parsedNames.push(parsedName);
|
|
nextOrder += 1;
|
|
}
|
|
if (parsedNames.length === 0) return null;
|
|
const record: ParsedPythonImport = {
|
|
raw: collected.join(eol),
|
|
source: match[1],
|
|
names: parsedNames.map(formatImportedName),
|
|
parsedNames,
|
|
kind: "from",
|
|
group: classifyGroup(match[1]),
|
|
order: startOrder,
|
|
multiline: sawParenthesis || collected.length > 1,
|
|
startLine: startIndex + 1,
|
|
endLine: nextIndex,
|
|
};
|
|
return { record, nextIndex, nextOrder };
|
|
}
|
|
|
|
function compareImportedNames(left: ImportedName, right: ImportedName): number {
|
|
const leftKey = `${left.name}|${left.alias ?? ""}`;
|
|
const rightKey = `${right.name}|${right.alias ?? ""}`;
|
|
return leftKey.localeCompare(rightKey);
|
|
}
|
|
|
|
function compareRecords(left: ParsedPythonImport, right: ParsedPythonImport): number {
|
|
const leftKey = left.kind === "module" ? `import ${left.source} ${left.alias ?? ""}` : `from ${left.source}`;
|
|
const rightKey = right.kind === "module" ? `import ${right.source} ${right.alias ?? ""}` : `from ${right.source}`;
|
|
return leftKey.localeCompare(rightKey);
|
|
}
|
|
|
|
function hasBlankLineBetween(lines: string[], previousEndLine: number, nextStartLine: number): boolean {
|
|
for (let index = previousEndLine; index < nextStartLine - 1; index += 1) {
|
|
if (isBlankLine(lines[index] ?? "")) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function inferSorted(records: ParsedPythonImport[]): boolean {
|
|
const byGroup = new Map<ImportGroup, ParsedPythonImport[]>();
|
|
for (const record of records) {
|
|
const groupRecords = byGroup.get(record.group);
|
|
if (groupRecords) groupRecords.push(record);
|
|
else byGroup.set(record.group, [record]);
|
|
if (record.kind === "from") {
|
|
for (let index = 1; index < record.parsedNames.length; index += 1) {
|
|
if (compareImportedNames(record.parsedNames[index - 1]!, record.parsedNames[index]!) > 0) return false;
|
|
}
|
|
}
|
|
}
|
|
for (const group of DEFAULT_GROUP_ORDER) {
|
|
const groupRecords = byGroup.get(group) ?? [];
|
|
for (let index = 1; index < groupRecords.length; index += 1) {
|
|
if (compareRecords(groupRecords[index - 1]!, groupRecords[index]!) > 0) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function inferStyle(records: ParsedPythonImport[], lines: string[]): PythonImportStyle {
|
|
const groupOrder: ImportGroup[] = [];
|
|
let groupSeparator = false;
|
|
for (let index = 0; index < records.length; index += 1) {
|
|
const record = records[index]!;
|
|
if (!groupOrder.includes(record.group)) groupOrder.push(record.group);
|
|
if (index > 0) {
|
|
const previous = records[index - 1]!;
|
|
if (hasBlankLineBetween(lines, previous.endLine, record.startLine)) groupSeparator = true;
|
|
}
|
|
}
|
|
return {
|
|
...defaultImportStyle,
|
|
groupSeparator,
|
|
groupOrder: groupOrder.length > 0 ? groupOrder : [...DEFAULT_GROUP_ORDER],
|
|
sorted: inferSorted(records),
|
|
parenthesized: records.some(record => record.kind === "from" && record.multiline),
|
|
};
|
|
}
|
|
|
|
function normalizeRecords(records: ParsedPythonImport[]): ParsedPythonImport[] {
|
|
const moduleRecords = new Map<string, ParsedPythonImport>();
|
|
const fromRecords = new Map<string, ParsedPythonImport>();
|
|
for (const record of [...records].sort((left, right) => left.order - right.order)) {
|
|
if (record.kind === "module") {
|
|
const key = `${record.source}|${record.alias ?? ""}`;
|
|
if (!moduleRecords.has(key)) moduleRecords.set(key, { ...record, parsedNames: [], names: [] });
|
|
continue;
|
|
}
|
|
const existing = fromRecords.get(record.source);
|
|
if (!existing) {
|
|
fromRecords.set(record.source, {
|
|
...record,
|
|
parsedNames: [...record.parsedNames],
|
|
names: [...record.names],
|
|
});
|
|
continue;
|
|
}
|
|
for (const name of record.parsedNames) {
|
|
const alreadyPresent = existing.parsedNames.some(
|
|
existingName => existingName.name === name.name && (existingName.alias ?? "") === (name.alias ?? ""),
|
|
);
|
|
if (alreadyPresent) continue;
|
|
existing.parsedNames.push({ ...name, order: existing.parsedNames.length });
|
|
}
|
|
existing.names = existing.parsedNames.map(formatImportedName);
|
|
existing.multiline ||= record.multiline;
|
|
if (record.order < existing.order) existing.order = record.order;
|
|
}
|
|
const combined = [...moduleRecords.values(), ...fromRecords.values()];
|
|
return combined.sort((left, right) => left.order - right.order);
|
|
}
|
|
|
|
function renderRecord(record: ParsedPythonImport, style: PythonImportStyle): string {
|
|
if (record.kind === "module") {
|
|
return record.alias ? `import ${record.source} as ${record.alias}` : `import ${record.source}`;
|
|
}
|
|
const names = [...record.parsedNames];
|
|
if (style.sorted) names.sort(compareImportedNames);
|
|
const renderedNames = names.map(formatImportedName);
|
|
const useParenthesized = record.multiline || (style.parenthesized && renderedNames.length > 1);
|
|
if (!useParenthesized) {
|
|
return `from ${record.source} import ${renderedNames.join(", ")}`;
|
|
}
|
|
return [`from ${record.source} import (`, ...renderedNames.map(name => ` ${name},`), `)`].join("\n");
|
|
}
|
|
|
|
function groupComparator(left: ImportGroup, right: ImportGroup, style: PythonImportStyle): number {
|
|
const orderedGroups = [
|
|
...style.groupOrder,
|
|
...DEFAULT_GROUP_ORDER.filter(group => !style.groupOrder.includes(group)),
|
|
];
|
|
return orderedGroups.indexOf(left) - orderedGroups.indexOf(right);
|
|
}
|
|
|
|
function renderImportBlock(records: ParsedPythonImport[], style: PythonImportStyle): string[] {
|
|
const normalized = normalizeRecords(records).map(record => ({
|
|
...record,
|
|
parsedNames: [...record.parsedNames],
|
|
names: [...record.names],
|
|
}));
|
|
const grouped = new Map<ImportGroup, ParsedPythonImport[]>();
|
|
for (const record of normalized) {
|
|
const groupRecords = grouped.get(record.group);
|
|
if (groupRecords) groupRecords.push(record);
|
|
else grouped.set(record.group, [record]);
|
|
}
|
|
const orderedGroups = [...grouped.keys()].sort((left, right) => groupComparator(left, right, style));
|
|
const renderedLines: string[] = [];
|
|
for (const group of orderedGroups) {
|
|
const groupRecords = grouped.get(group) ?? [];
|
|
const orderedRecords = style.sorted
|
|
? [...groupRecords].sort(compareRecords)
|
|
: [...groupRecords].sort((left, right) => left.order - right.order);
|
|
if (renderedLines.length > 0 && style.groupSeparator) renderedLines.push("");
|
|
for (const record of orderedRecords) {
|
|
renderedLines.push(...renderRecord(record, style).split("\n"));
|
|
}
|
|
}
|
|
return renderedLines;
|
|
}
|
|
|
|
function specToRecords(spec: ImportSpec, order: number): { records: ParsedPythonImport[]; warnings: string[] } {
|
|
const warnings: string[] = [];
|
|
if (spec.default) warnings.push(`Python imports do not support default import syntax for ${spec.from}`);
|
|
if (spec.namespace) warnings.push(`Python imports do not support namespace import syntax for ${spec.from}`);
|
|
if (spec.imports && spec.imports.length > 0) {
|
|
const parsedNames = spec.imports.flatMap((item, index) => {
|
|
const parsed = parseImportedName(item, order + index);
|
|
if (parsed) return [parsed];
|
|
return [];
|
|
});
|
|
if (parsedNames.length === 1 && spec.alias) parsedNames[0] = { ...parsedNames[0]!, alias: spec.alias };
|
|
if (parsedNames.length === 0) return { records: [], warnings };
|
|
return {
|
|
records: [
|
|
{
|
|
raw: "",
|
|
source: spec.from,
|
|
names: parsedNames.map(formatImportedName),
|
|
parsedNames,
|
|
kind: "from",
|
|
group: classifyGroup(spec.from),
|
|
order,
|
|
multiline: false,
|
|
startLine: 0,
|
|
endLine: 0,
|
|
},
|
|
],
|
|
warnings,
|
|
};
|
|
}
|
|
return {
|
|
records: [
|
|
{
|
|
raw: "",
|
|
source: spec.from,
|
|
names: [],
|
|
parsedNames: [],
|
|
alias: spec.alias,
|
|
kind: "module",
|
|
group: classifyGroup(spec.from),
|
|
order,
|
|
multiline: false,
|
|
startLine: 0,
|
|
endLine: 0,
|
|
},
|
|
],
|
|
warnings,
|
|
};
|
|
}
|
|
|
|
function parsePythonRegion(content: string): ParsedPythonRegion | null {
|
|
const lines = content.split(/\r?\n/);
|
|
const eol = detectEol(content);
|
|
const prefixEnd = findPrefixEnd(lines);
|
|
let index = prefixEnd;
|
|
while (index < lines.length && (isBlankLine(lines[index] ?? "") || isCommentLine(lines[index] ?? ""))) index += 1;
|
|
if (!isImportStart(lines[index] ?? "")) return null;
|
|
const records: ParsedPythonImport[] = [];
|
|
let nextOrder = 0;
|
|
while (index < lines.length) {
|
|
const currentLine = lines[index] ?? "";
|
|
if (isBlankLine(currentLine) || isCommentLine(currentLine)) {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (currentLine.trimStart().startsWith("import ")) {
|
|
const parsed = parseImportStatement(currentLine, index + 1, nextOrder);
|
|
if (!parsed) break;
|
|
records.push(...parsed.records);
|
|
nextOrder = parsed.nextOrder;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (currentLine.trimStart().startsWith("from ")) {
|
|
const parsed = parseFromStatement(lines, index, eol, nextOrder);
|
|
if (!parsed) break;
|
|
records.push(parsed.record);
|
|
nextOrder = parsed.nextOrder;
|
|
index = parsed.nextIndex;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
if (records.length === 0) return null;
|
|
const normalizedRecords = normalizeRecords(records);
|
|
return {
|
|
startLine: normalizedRecords[0]!.startLine,
|
|
endLine: Math.max(...normalizedRecords.map(record => record.endLine)),
|
|
imports: normalizedRecords,
|
|
style: inferStyle(normalizedRecords, lines),
|
|
};
|
|
}
|
|
|
|
function parseExisting(content: string): ImportRegion | null {
|
|
return parsePythonRegion(content);
|
|
}
|
|
|
|
function specToLine(spec: ImportSpec, style: ImportStyle): string {
|
|
const pythonStyle = getPythonStyle(style);
|
|
const { records } = specToRecords(spec, 0);
|
|
return records.length === 0 ? "" : renderRecord(records[0]!, pythonStyle);
|
|
}
|
|
|
|
function mergeRecords(
|
|
existing: ParsedPythonImport[],
|
|
requested: ParsedPythonImport[],
|
|
style: PythonImportStyle,
|
|
): { records: ParsedPythonImport[]; added: string[] } {
|
|
const records = normalizeRecords(existing).map(record => ({
|
|
...record,
|
|
parsedNames: [...record.parsedNames],
|
|
names: [...record.names],
|
|
}));
|
|
let nextOrder = records.reduce((max, record) => Math.max(max, record.order), -1) + 1;
|
|
const added: string[] = [];
|
|
for (const record of requested) {
|
|
if (record.kind === "module") {
|
|
const exists = records.some(
|
|
existingRecord =>
|
|
existingRecord.kind === "module" &&
|
|
existingRecord.source === record.source &&
|
|
(existingRecord.alias ?? "") === (record.alias ?? ""),
|
|
);
|
|
if (exists) continue;
|
|
records.push({ ...record, order: nextOrder, startLine: 0, endLine: 0 });
|
|
added.push(renderRecord(record, style));
|
|
nextOrder += 1;
|
|
continue;
|
|
}
|
|
const existingRecord = records.find(candidate => candidate.kind === "from" && candidate.source === record.source);
|
|
if (!existingRecord || existingRecord.kind !== "from") {
|
|
records.push({ ...record, order: nextOrder, startLine: 0, endLine: 0 });
|
|
added.push(renderRecord(record, style));
|
|
nextOrder += 1;
|
|
continue;
|
|
}
|
|
const missingNames = record.parsedNames.filter(
|
|
name =>
|
|
!existingRecord.parsedNames.some(
|
|
existingName => existingName.name === name.name && (existingName.alias ?? "") === (name.alias ?? ""),
|
|
),
|
|
);
|
|
if (missingNames.length === 0) continue;
|
|
for (const name of missingNames) {
|
|
existingRecord.parsedNames.push({ ...name, order: existingRecord.parsedNames.length });
|
|
}
|
|
if (style.sorted) existingRecord.parsedNames.sort(compareImportedNames);
|
|
existingRecord.names = existingRecord.parsedNames.map(formatImportedName);
|
|
existingRecord.multiline ||= record.multiline;
|
|
added.push(
|
|
renderRecord(
|
|
{ ...existingRecord, parsedNames: missingNames, names: missingNames.map(formatImportedName) },
|
|
style,
|
|
),
|
|
);
|
|
}
|
|
return { records, added };
|
|
}
|
|
|
|
function insertWithoutRegion(content: string, lines: string[], blockLines: string[], eol: string): string {
|
|
if (content.length === 0) return blockLines.join(eol);
|
|
const insertAt = findPrefixEnd(lines);
|
|
const before = lines.slice(0, insertAt);
|
|
const after = lines.slice(insertAt);
|
|
const needsTrailingBlank =
|
|
after.length > 0 && after.some(line => line.trim().length > 0) && !isBlankLine(after[0] ?? "");
|
|
const inserted = [...before, ...blockLines, ...(needsTrailingBlank ? [""] : []), ...after];
|
|
const nextContent = inserted.join(eol);
|
|
if (content.endsWith(eol) && !nextContent.endsWith(eol)) return `${nextContent}${eol}`;
|
|
return nextContent;
|
|
}
|
|
|
|
function replaceRegion(
|
|
content: string,
|
|
lines: string[],
|
|
region: ParsedPythonRegion,
|
|
blockLines: string[],
|
|
eol: string,
|
|
): string {
|
|
const nextLines = [...lines.slice(0, region.startLine - 1), ...blockLines, ...lines.slice(region.endLine)];
|
|
const nextContent = nextLines.join(eol);
|
|
if (content.endsWith(eol) && !nextContent.endsWith(eol)) return `${nextContent}${eol}`;
|
|
return nextContent;
|
|
}
|
|
|
|
export const pythonImportHandler: ImportHandler = {
|
|
parseExisting,
|
|
specToLine,
|
|
apply(content, specs) {
|
|
const region = parsePythonRegion(content);
|
|
const style = getPythonStyle(
|
|
region?.style ?? {
|
|
...defaultImportStyle,
|
|
groupSeparator: true,
|
|
groupOrder: [...DEFAULT_GROUP_ORDER],
|
|
sorted: true,
|
|
parenthesized: false,
|
|
},
|
|
);
|
|
const warnings: string[] = [];
|
|
const requested: ParsedPythonImport[] = [];
|
|
let order = 0;
|
|
for (const spec of specs) {
|
|
const result = specToRecords(spec, order);
|
|
warnings.push(...result.warnings);
|
|
requested.push(...result.records);
|
|
order += Math.max(result.records.length, 1);
|
|
}
|
|
if (requested.length === 0) return { content, added: [], warnings };
|
|
const { records, added } = mergeRecords(region?.imports ?? [], requested, style);
|
|
if (added.length === 0) return { content, added: [], warnings };
|
|
const eol = detectEol(content);
|
|
const lines = content.split(/\r?\n/);
|
|
const blockLines = renderImportBlock(records, style);
|
|
const nextContent = region
|
|
? replaceRegion(content, lines, region, blockLines, eol)
|
|
: insertWithoutRegion(content, lines, blockLines, eol);
|
|
return { content: nextContent, added, warnings };
|
|
},
|
|
};
|
|
|
|
export { defaultImportStyle };
|