import { CodeBlock, insertContentsAtOffset, searchFromOffset } from '../utils/commonCodeMod'; import { findMatchingBracketPosition } from '../utils/matchBrackets'; interface SwiftFunctionParam { argumentLabel: string; parameterName: string; typeString: string; } interface InsertContentFunctionOptions { position: 'head' | 'tail' | 'tailBeforeLastReturn'; indent?: number; } /** * Add Objective-C import * @param source source contents * @param imports array of imports, e.g. [''] * @returns updated contents */ export function addObjcImports(source: string, imports: string[]): string { const lines = source.split('\n'); // Try to insert statements after first #import where would probably not in #if block const lineIndexWithFirstImport = lines.findIndex((line) => line.match(/^#import .*$/)); for (const importElement of imports) { if (!source.includes(importElement)) { const importStatement = `#import ${importElement}`; lines.splice(lineIndexWithFirstImport + 1, 0, importStatement); } } return lines.join('\n'); } /** * Find code block of Objective-C interface or implementation * * @param contents source contents * @param declaration interface/implementation, e.g. '@interface Foo' * @returns found CodeBlock, or null if not found */ export function findObjcInterfaceCodeBlock( contents: string, declaration: string ): CodeBlock | null { const start = contents.search(new RegExp(`^${declaration}\\W`, 'm')); if (start < 0) { return null; } let end = contents.indexOf('\n@end', start); end += 5; // '\n@end'.length === 5 return { start, end, code: contents.substring(start, end), }; } /** * Find code block of Objective-C function without declaration, will return only {} block * * @param contents source contents * @param selector function selector, e.g. 'doSomething:withSomeValue:' * @returns found CodeBlock, or null if not found. */ export function findObjcFunctionCodeBlock(contents: string, selector: string): CodeBlock | null { const symbols = selector.split(':'); const argsCount = symbols.length - 1; let pattern = '^[\\-+]\\s*\\(.+?\\)'; if (argsCount === 0) { pattern += `${symbols[0]}\\s+`; } else { for (let i = 0; i < argsCount; ++i) { const argSymbol = `${symbols[i]}:\\(.+\\)\\w+`; pattern += `${argSymbol}\\s+`; } } pattern += '{'; let start = contents.search(new RegExp(pattern, 'm')); if (start < 0) { return null; } start = contents.indexOf('{', start); const end = findMatchingBracketPosition(contents, '{', start); return { start, end, code: contents.substring(start, end + 1), }; } /** * Insert contents to the Objective-C function block * * @param srcContents source contents * @param selector function selector, e.g. 'doSomething:withSomeValue:' * @param insertion code to insert * @param options insertion options * @returns updated contents */ export function insertContentsInsideObjcFunctionBlock( srcContents: string, selector: string, insertion: string, options: InsertContentFunctionOptions ): string { return insertContentsInsideFunctionBlock(srcContents, selector, insertion, options, 'objc'); } /** * Insert contents to the Objective-C interface/implementation block * * @param srcContents source contents * @param declaration interface/implementation, e.g. '@interface Foo' * @param insertion code to insert * @param options insertion options * @returns updated contents */ export function insertContentsInsideObjcInterfaceBlock( srcContents: string, declaration: string, insertion: string, options: { position: 'head' | 'tail'; } ): string { const codeBlock = findObjcInterfaceCodeBlock(srcContents, declaration); if (!codeBlock) { return srcContents; } const { position } = options; if (position === 'head') { const firstNewLineIndex = srcContents.indexOf('\n', codeBlock.start); srcContents = insertContentsAtOffset(srcContents, insertion, firstNewLineIndex); } else if (position === 'tail') { const endLen = '@end'.length; srcContents = insertContentsAtOffset(srcContents, insertion, codeBlock.end - endLen); } return srcContents; } /** * Find code block of Swift function without declaration, will return only {} block * * @param contents source contents * @param selector function selector, e.g. 'doSomething(_:withSomeValue:)' * @returns found CodeBlock, or null if not found. */ export function findSwiftFunctionCodeBlock(contents: string, selector: string): CodeBlock | null { const parenthesesIndex = selector.indexOf('('); // `functName` === 'doSomething' of 'doSomething(_:withSomeValue:)' const funcName = selector.substring(0, parenthesesIndex); // `argLabels` === ['_', 'withSomeValue'] 'doSomething(_:withSomeValue:)' const argLabels = selector.substring(parenthesesIndex + 1, selector.length - 2).split(':'); let searchOffset = 0; const funcCandidateRegExp = new RegExp(`\\sfunc\\s+${funcName}\\(`, 'm'); let funcCandidateOffset = searchFromOffset(contents, funcCandidateRegExp, searchOffset); while (funcCandidateOffset >= 0) { // Parse function parameters const paramsStartOffset = contents.indexOf('(', funcCandidateOffset); const paramsEndOffset = findMatchingBracketPosition(contents, '(', paramsStartOffset); const paramsString = contents.substring(paramsStartOffset + 1, paramsEndOffset); const params = paramsString.split(',').map(parseSwiftFunctionParam); // Prepare offset for next round searchOffset = paramsEndOffset + 1; funcCandidateOffset = searchFromOffset(contents, funcCandidateRegExp, searchOffset); // Try to match function parameters if (argLabels.length !== params.length) { continue; } for (let i = 0; i < argLabels.length; ++i) { if (argLabels[i] !== params[i].argumentLabel) { continue; } } // This function is matched one, get the code block. const codeBlockStart = contents.indexOf('{', paramsEndOffset); const codeBlockEnd = findMatchingBracketPosition(contents, '{', paramsEndOffset); const codeBlock = contents.substring(codeBlockStart, codeBlockEnd + 1); return { start: codeBlockStart, end: codeBlockEnd, code: codeBlock, }; } return null; } function parseSwiftFunctionParam(paramTuple: string): SwiftFunctionParam { const semiIndex = paramTuple.indexOf(':'); const [argumentLabel, parameterName] = paramTuple.substring(0, semiIndex).split(/\s+/); const typeString = paramTuple.substring(semiIndex + 1).trim(); return { argumentLabel, parameterName, typeString, }; } /** * Insert contents to the swift class block * * @param srcContents source contents * @param declaration class/extension declaration, e.g. 'class AppDelegate' * @param insertion code to append * @param options insertion options * @returns updated contents */ export function insertContentsInsideSwiftClassBlock( srcContents: string, declaration: string, insertion: string, options: { position: 'head' | 'tail'; } ): string { const start = srcContents.search(new RegExp(`\\s*${declaration}.*?[\\(\\{]`)); if (start < 0) { throw new Error(`Unable to find class code block - declaration[${declaration}]`); } const { position } = options; if (position === 'head') { const firstBracketIndex = srcContents.indexOf('{', start); srcContents = insertContentsAtOffset(srcContents, insertion, firstBracketIndex + 1); } else if (position === 'tail') { const endBracketIndex = findMatchingBracketPosition(srcContents, '{', start); srcContents = insertContentsAtOffset(srcContents, insertion, endBracketIndex); } return srcContents; } /** * Insert contents to the Swift function block * * @param srcContents source contents * @param selector function selector, e.g. 'doSomething:withSomeValue:' * @param insertion code to insert * @param options insertion options * @returns updated contents */ export function insertContentsInsideSwiftFunctionBlock( srcContents: string, selector: string, insertion: string, options: InsertContentFunctionOptions ): string { return insertContentsInsideFunctionBlock(srcContents, selector, insertion, options, 'swift'); } function insertContentsInsideFunctionBlock( srcContents: string, selector: string, insertion: string, options: InsertContentFunctionOptions, language: 'objc' | 'swift' ): string { const codeBlock = language === 'objc' ? findObjcFunctionCodeBlock(srcContents, selector) : findSwiftFunctionCodeBlock(srcContents, selector); if (!codeBlock) { return srcContents; } const { position } = options; const indent = ' '.repeat(options.indent ?? 2); if (position === 'head') { srcContents = insertContentsAtOffset( srcContents, `\n${indent}${insertion}`, codeBlock.start + 1 ); } else if (position === 'tail') { srcContents = insertContentsAtOffset(srcContents, `\n${indent}${insertion}`, codeBlock.end - 1); } else if (position === 'tailBeforeLastReturn') { let lastReturnIndex = srcContents.lastIndexOf(' return ', codeBlock.end); if (lastReturnIndex < 0) { throw new Error(`Cannot find last return statement:\n${srcContents}`); } lastReturnIndex += 1; // +1 for the prefix space srcContents = insertContentsAtOffset(srcContents, `${insertion}\n${indent}`, lastReturnIndex); } return srcContents; }