"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DtsDocumentSnapshot = exports.JSOrTSDocumentSnapshot = exports.SvelteDocumentSnapshot = exports.DocumentSnapshot = exports.INITIAL_VERSION = void 0;
exports.computeChangeRange = computeChangeRange;
const trace_mapping_1 = require("@jridgewell/trace-mapping");
const svelte2tsx_1 = require("svelte2tsx");
const typescript_1 = __importDefault(require("typescript"));
const documents_1 = require("../../lib/documents");
const utils_1 = require("../../utils");
const DocumentMapper_1 = require("./DocumentMapper");
const svelte_ast_utils_1 = require("./svelte-ast-utils");
const utils_2 = require("./utils");
const logger_1 = require("../../logger");
const path_1 = require("path");
const vscode_uri_1 = require("vscode-uri");
const utils_3 = require("./features/utils");
const configLoader_1 = require("../../lib/documents/configLoader");
/**
 * Computes the change range between two snapshots by scanning for the first and last
 * differing characters. This allows TypeScript to do incremental re-parsing instead of
 * a full reparse when a snapshot changes.
 */
function computeChangeRange(oldText, newText) {
    const minLen = Math.min(oldText.length, newText.length);
    // Scan from the front to find the first differing character
    let prefixLen = 0;
    while (prefixLen < minLen && oldText.charCodeAt(prefixLen) === newText.charCodeAt(prefixLen)) {
        prefixLen++;
    }
    // Scan from the back to find the last differing character
    let oldSuffixStart = oldText.length;
    let newSuffixStart = newText.length;
    while (oldSuffixStart > prefixLen &&
        newSuffixStart > prefixLen &&
        oldText.charCodeAt(oldSuffixStart - 1) === newText.charCodeAt(newSuffixStart - 1)) {
        oldSuffixStart--;
        newSuffixStart--;
    }
    return {
        span: { start: prefixLen, length: oldSuffixStart - prefixLen },
        newLength: newSuffixStart - prefixLen
    };
}
/**
 * Initial version of snapshots.
 */
exports.INITIAL_VERSION = 0;
const ambientPathPattern = /node_modules[\/\\]svelte[\/\\]types[\/\\]ambient\.d\.ts$/;
const svelteTypesPattern = /node_modules[\/\\]svelte[\/\\]types[\/\\]index\.d\.ts$/;
const shimsPattern = /svelte2tsx[\/\\]svelte-shims\.d\.ts$|svelte-check[\/\\]dist[\/\\]src[\/\\]svelte-shims\.d\.ts$/;
var DocumentSnapshot;
(function (DocumentSnapshot) {
    /**
     * Returns a svelte snapshot from a svelte document.
     * @param document the svelte document
     * @param options options that apply to the svelte document
     */
    function fromDocument(document, options) {
        const { tsxMap, htmlAst, text, exportedNames, parserError, nrPrependedLines, scriptKind } = preprocessSvelteFile(document, options);
        return new SvelteDocumentSnapshot(document, parserError, scriptKind, options.version, text, nrPrependedLines, exportedNames, tsxMap, htmlAst);
    }
    DocumentSnapshot.fromDocument = fromDocument;
    /**
     * Returns a svelte or ts/js snapshot from a file path, depending on the file contents.
     * @param filePath path to the js/ts/svelte file
     * @param createDocument function that is used to create a document in case it's a Svelte file
     * @param options options that apply in case it's a svelte file
     */
    function fromFilePath(filePath, createDocument, options, tsSystem) {
        if ((0, utils_2.isSvelteFilePath)(filePath)) {
            return DocumentSnapshot.fromSvelteFilePath(filePath, createDocument, options, tsSystem);
        }
        else {
            return DocumentSnapshot.fromNonSvelteFilePath(filePath, tsSystem);
        }
    }
    DocumentSnapshot.fromFilePath = fromFilePath;
    /**
     * Returns a ts/js snapshot from a file path.
     * @param filePath path to the js/ts file
     * @param options options that apply in case it's a svelte file
     */
    function fromNonSvelteFilePath(filePath, tsSystem) {
        // The following (very hacky) code makes sure that the ambient module definitions
        // that tell TS "every import ending with .svelte is a valid module" are removed.
        // They exist in svelte2tsx and svelte to make sure that people don't
        // get errors in their TS files when importing Svelte files and not using our TS plugin.
        // If someone wants to get back the behavior they can add an ambient module definition
        // on their own.
        let originalText = '';
        if (!ambientPathPattern.test(filePath)) {
            originalText = tsSystem.readFile(filePath) || '';
        }
        if (svelteTypesPattern.test(filePath)) {
            const startIdx = originalText.indexOf(`declare module '*.svelte' {`);
            const endIdx = originalText.indexOf(`\n}`, startIdx + 1) + 2;
            originalText =
                originalText.substring(0, startIdx) +
                    ' '.repeat(endIdx - startIdx) +
                    originalText.substring(endIdx);
        }
        else if (shimsPattern.test(filePath)) {
            // If not present, the LS uses an older version of svelte2tsx
            if (originalText.includes('// -- start svelte-ls-remove --')) {
                originalText =
                    originalText.substring(0, originalText.indexOf('// -- start svelte-ls-remove --')) +
                        originalText.substring(originalText.indexOf('// -- end svelte-ls-remove --'));
            }
        }
        const declarationExtensions = [typescript_1.default.Extension.Dcts, typescript_1.default.Extension.Dts, typescript_1.default.Extension.Dmts];
        if (declarationExtensions.some((ext) => filePath.endsWith(ext))) {
            return new DtsDocumentSnapshot(exports.INITIAL_VERSION, filePath, originalText, tsSystem);
        }
        return new JSOrTSDocumentSnapshot(exports.INITIAL_VERSION, filePath, originalText);
    }
    DocumentSnapshot.fromNonSvelteFilePath = fromNonSvelteFilePath;
    /**
     * Returns a svelte snapshot from a file path.
     * @param filePath path to the svelte file
     * @param createDocument function that is used to create a document
     * @param options options that apply in case it's a svelte file
     */
    function fromSvelteFilePath(filePath, createDocument, options, tsSystem) {
        const originalText = tsSystem.readFile(filePath) ?? '';
        return fromDocument(createDocument(filePath, originalText), options);
    }
    DocumentSnapshot.fromSvelteFilePath = fromSvelteFilePath;
})(DocumentSnapshot || (exports.DocumentSnapshot = DocumentSnapshot = {}));
/**
 * Tries to preprocess the svelte document and convert the contents into better analyzable js/ts(x) content.
 */
function preprocessSvelteFile(document, options) {
    let tsxMap;
    let parserError = null;
    let nrPrependedLines = 0;
    let text = document.getText();
    let exportedNames = { has: () => false };
    let htmlAst;
    const scriptKind = [
        (0, utils_2.getScriptKindFromAttributes)(document.scriptInfo?.attributes ?? {}),
        (0, utils_2.getScriptKindFromAttributes)(document.moduleScriptInfo?.attributes ?? {})
    ].includes(typescript_1.default.ScriptKind.TSX)
        ? typescript_1.default.ScriptKind.TS
        : typescript_1.default.ScriptKind.JS;
    try {
        const tsx = (0, svelte2tsx_1.svelte2tsx)(text, {
            parse: options.parse,
            version: options.version,
            filename: document.getFilePath() ?? undefined,
            isTsFile: scriptKind === typescript_1.default.ScriptKind.TS,
            mode: 'ts',
            typingsNamespace: options.typingsNamespace,
            emitOnTemplateError: options.transformOnTemplateError,
            namespace: document.config?.compilerOptions?.namespace,
            accessors: document.config?.compilerOptions?.accessors ??
                (typeof document.config?.compilerOptions?.customElement === 'function'
                    ? // @ts-ignore with Svelte 4 this is never callable
                        document.config.compilerOptions.customElement({
                            filename: document.getFilePath() ?? ''
                        })
                    : document.config?.compilerOptions?.customElement),
            emitJsDoc: options.emitJsDoc,
            rewriteExternalImports: options.rewriteExternalImports
        });
        text = tsx.code;
        tsxMap = tsx.map;
        exportedNames = tsx.exportedNames;
        // We know it's there, it's not part of the public API so people don't start using it
        htmlAst = tsx.htmlAst;
        if (tsxMap) {
            tsxMap.sources = [document.uri];
            const scriptInfo = document.scriptInfo || document.moduleScriptInfo;
            const tsCheck = (0, utils_2.getTsCheckComment)(scriptInfo?.content);
            if (tsCheck) {
                text = tsCheck + text;
                nrPrependedLines = 1;
            }
        }
    }
    catch (e) {
        // Error start/end logic is different and has different offsets for line, so we need to convert that
        const start = {
            line: (e.start?.line ?? 1) - 1,
            character: e.start?.column ?? 0
        };
        const end = e.end ? { line: e.end.line - 1, character: e.end.column } : start;
        parserError = {
            range: { start, end },
            message: e.message,
            code: -1
        };
        // fall back to extracted script, if any
        const scriptInfo = document.scriptInfo || document.moduleScriptInfo;
        text = scriptInfo ? scriptInfo.content : '';
    }
    return {
        tsxMap,
        text,
        exportedNames,
        htmlAst,
        parserError,
        nrPrependedLines,
        scriptKind
    };
}
/**
 * A svelte document snapshot suitable for the TS language service and the plugin.
 * It contains the generated code (Svelte->TS/JS) so the TS language service can understand it.
 */
class SvelteDocumentSnapshot {
    constructor(parent, parserError, scriptKind, svelteVersion, text, nrPrependedLines, exportedNames, tsxMap, htmlAst) {
        this.parent = parent;
        this.parserError = parserError;
        this.scriptKind = scriptKind;
        this.svelteVersion = svelteVersion;
        this.text = text;
        this.nrPrependedLines = nrPrependedLines;
        this.exportedNames = exportedNames;
        this.tsxMap = tsxMap;
        this.htmlAst = htmlAst;
        this.url = (0, utils_1.pathToUrl)(this.filePath);
        this.version = this.parent.version;
        this.isSvelte5Plus = Number(this.svelteVersion?.split('.')[0]) >= 5;
    }
    get filePath() {
        return this.parent.getFilePath() || '';
    }
    get scriptInfo() {
        return this.parent.scriptInfo;
    }
    get moduleScriptInfo() {
        return this.parent.moduleScriptInfo;
    }
    getOriginalText(range) {
        return this.parent.getText(range);
    }
    getText(start, end) {
        return this.text.substring(start, end);
    }
    getLength() {
        return this.text.length;
    }
    getFullText() {
        return this.text;
    }
    getChangeRange(oldSnapshot) {
        if (oldSnapshot instanceof SvelteDocumentSnapshot) {
            if (oldSnapshot.scriptKind !== this.scriptKind ||
                !!oldSnapshot.parserError !== !!this.parserError) {
                return undefined;
            }
        }
        return computeChangeRange(oldSnapshot.getText(0, oldSnapshot.getLength()), this.text);
    }
    positionAt(offset) {
        return (0, documents_1.positionAt)(offset, this.text, this.getLineOffsets());
    }
    offsetAt(position) {
        return (0, documents_1.offsetAt)(position, this.text, this.getLineOffsets());
    }
    getLineContainingOffset(offset) {
        const chunks = this.getText(0, offset).split('\n');
        return chunks[chunks.length - 1];
    }
    hasProp(name) {
        return this.exportedNames.has(name);
    }
    svelteNodeAt(positionOrOffset) {
        if (!this.htmlAst) {
            return null;
        }
        const offset = typeof positionOrOffset === 'number'
            ? positionOrOffset
            : this.parent.offsetAt(positionOrOffset);
        let foundNode = null;
        this.walkSvelteAst({
            enter(node) {
                // In case the offset is at a point where a node ends and a new one begins,
                // the node where the code ends is used. If this introduces problems, introduce
                // an affinity parameter to prefer the node where it ends/starts.
                if (node.start > offset || node.end < offset) {
                    this.skip();
                    return;
                }
                const parent = foundNode;
                // Use Object.create so the "parent" property isn't added to the original ast,
                // causing an infinite loop. This is cheaper than spreading all properties.
                foundNode = Object.create(node);
                if (parent) {
                    foundNode.parent = parent;
                }
            }
        });
        return foundNode;
    }
    walkSvelteAst(walker) {
        if (!this.htmlAst) {
            return;
        }
        (0, svelte_ast_utils_1.walkSvelteAst)(this.htmlAst, walker);
    }
    getOriginalPosition(pos) {
        return this.getMapper().getOriginalPosition(pos);
    }
    getGeneratedPosition(pos) {
        return this.getMapper().getGeneratedPosition(pos);
    }
    isInGenerated(pos) {
        return !(0, documents_1.isInTag)(pos, this.parent.styleInfo);
    }
    getURL() {
        return this.url;
    }
    isOpenedInClient() {
        return this.parent.openedByClient;
    }
    getLineOffsets() {
        if (!this.lineOffsets) {
            this.lineOffsets = (0, documents_1.getLineOffsets)(this.text);
        }
        return this.lineOffsets;
    }
    getMapper() {
        if (!this.mapper) {
            this.mapper = this.initMapper();
        }
        return this.mapper;
    }
    initMapper() {
        const scriptInfo = this.parent.scriptInfo || this.parent.moduleScriptInfo;
        if (!this.tsxMap) {
            if (!scriptInfo) {
                return new documents_1.IdentityMapper(this.url);
            }
            return new documents_1.FragmentMapper(this.parent.getText(), scriptInfo, this.url);
        }
        return new DocumentMapper_1.ConsumerDocumentMapper(new trace_mapping_1.TraceMap(this.tsxMap), this.url, this.nrPrependedLines);
    }
}
exports.SvelteDocumentSnapshot = SvelteDocumentSnapshot;
/**
 * A js/ts document snapshot suitable for the ts language service and the plugin.
 * Since no mapping has to be done here, it also implements the mapper interface.
 * If it's a SvelteKit file (e.g. +page.ts), types will be auto-added if not explicitly typed.
 */
class JSOrTSDocumentSnapshot extends documents_1.IdentityMapper {
    isOpenedInClient() {
        return this.openedByClient;
    }
    constructor(version, filePath, text, openedByClient = false) {
        super((0, utils_1.pathToUrl)(filePath));
        this.version = version;
        this.filePath = filePath;
        this.text = text;
        this.openedByClient = openedByClient;
        this.scriptKind = (0, utils_2.getScriptKindFromFileName)(this.filePath);
        this.scriptInfo = null;
        this.originalText = this.text;
        this.kitFile = false;
        this.addedCode = [];
        this.paramsPath = 'src/params';
        this.serverHooksPath = 'src/hooks.server';
        this.clientHooksPath = 'src/hooks.client';
        this.universalHooksPath = 'src/hooks';
        this.adjustText();
    }
    getText(start, end) {
        return this.text.substring(start, end);
    }
    getLength() {
        return this.text.length;
    }
    getFullText() {
        return this.text;
    }
    getChangeRange(oldSnapshot) {
        return computeChangeRange(oldSnapshot.getText(0, oldSnapshot.getLength()), this.text);
    }
    positionAt(offset) {
        return (0, documents_1.positionAt)(offset, this.text, this.getLineOffsets());
    }
    offsetAt(position) {
        return (0, documents_1.offsetAt)(position, this.text, this.getLineOffsets());
    }
    getGeneratedPosition(originalPosition) {
        if (!this.kitFile || this.addedCode.length === 0) {
            return super.getGeneratedPosition(originalPosition);
        }
        const pos = this.originalOffsetAt(originalPosition);
        let total = 0;
        for (const added of this.addedCode) {
            if (pos < added.generatedPos)
                break;
            total += added.length;
        }
        return this.positionAt(pos + total);
    }
    getOriginalPosition(generatedPosition) {
        if (!this.kitFile || this.addedCode.length === 0) {
            return super.getOriginalPosition(generatedPosition);
        }
        const pos = this.offsetAt(generatedPosition);
        let total = 0;
        let idx = 0;
        for (; idx < this.addedCode.length; idx++) {
            const added = this.addedCode[idx];
            if (pos < added.generatedPos)
                break;
            total += added.length;
        }
        if (idx > 0) {
            const prev = this.addedCode[idx - 1];
            // Special case: pos is in the middle of an added range
            if (pos > prev.generatedPos && pos < prev.generatedPos + prev.length) {
                return this.originalPositionAt(prev.originalPos);
            }
        }
        return this.originalPositionAt(pos - total);
    }
    update(changes) {
        let updatedOriginalText = this.originalText;
        for (const change of changes) {
            let start = 0;
            let end = 0;
            if ('range' in change) {
                start = this.originalOffsetAt(change.range.start);
                end = this.originalOffsetAt(change.range.end);
            }
            else {
                end = updatedOriginalText.length;
            }
            updatedOriginalText =
                updatedOriginalText.slice(0, start) + change.text + updatedOriginalText.slice(end);
        }
        return new JSOrTSDocumentSnapshot(this.version + 1, this.filePath, updatedOriginalText, true // openedByClient
        );
    }
    getLineOffsets() {
        if (!this.lineOffsets) {
            this.lineOffsets = (0, documents_1.getLineOffsets)(this.text);
        }
        return this.lineOffsets;
    }
    originalOffsetAt(position) {
        return (0, documents_1.offsetAt)(position, this.originalText, this.getOriginalLineOffsets());
    }
    originalPositionAt(offset) {
        return (0, documents_1.positionAt)(offset, this.originalText, this.getOriginalLineOffsets());
    }
    getOriginalLineOffsets() {
        if (!this.kitFile) {
            return this.getLineOffsets();
        }
        if (!this.internalLineOffsets) {
            this.internalLineOffsets = (0, documents_1.getLineOffsets)(this.originalText);
        }
        return this.internalLineOffsets;
    }
    adjustText() {
        const result = svelte2tsx_1.internalHelpers.upsertKitFile(typescript_1.default, this.filePath, {
            clientHooksPath: this.clientHooksPath,
            paramsPath: this.paramsPath,
            serverHooksPath: this.serverHooksPath,
            universalHooksPath: this.universalHooksPath
        }, () => this.createSource(), utils_3.surroundWithIgnoreComments);
        if (!result) {
            this.kitFile = false;
            this.addedCode = [];
            this.text = this.originalText;
            return;
        }
        if (!this.kitFile) {
            const files = configLoader_1.configLoader.getConfig(this.filePath)?.kit?.files;
            if (files) {
                this.paramsPath ||= files.params;
                this.serverHooksPath ||= files.hooks?.server;
                this.clientHooksPath ||= files.hooks?.client;
                this.universalHooksPath ||= files.hooks?.universal;
            }
        }
        const { text, addedCode } = result;
        this.kitFile = true;
        this.addedCode = addedCode;
        this.text = text;
    }
    createSource() {
        return typescript_1.default.createSourceFile(this.filePath, this.originalText, typescript_1.default.ScriptTarget.Latest, true, this.scriptKind);
    }
}
exports.JSOrTSDocumentSnapshot = JSOrTSDocumentSnapshot;
const sourceMapCommentRegExp = /^\/\/[@#] source[M]appingURL=(.+)\r?\n?$/;
const whitespaceOrMapCommentRegExp = /^\s*(\/\/[@#] .*)?$/;
const base64UrlRegExp = /^data:(?:application\/json(?:;charset=[uU][tT][fF]-8);base64,([A-Za-z0-9+\/=]+)$)?/;
class DtsDocumentSnapshot extends JSOrTSDocumentSnapshot {
    constructor(version, filePath, text, tsSys) {
        super(version, filePath, text);
        this.tsSys = tsSys;
        this.mapperInitialized = false;
    }
    getOriginalFilePosition(generatedPosition) {
        if (!this.mapperInitialized) {
            this.traceMap = this.initMapper();
            this.mapperInitialized = true;
        }
        const mapped = this.traceMap
            ? (0, trace_mapping_1.originalPositionFor)(this.traceMap, {
                line: generatedPosition.line + 1,
                column: generatedPosition.character
            })
            : undefined;
        if (!mapped || mapped.line == null || !mapped.source) {
            return generatedPosition;
        }
        const originalFilePath = vscode_uri_1.URI.isUri(mapped.source)
            ? (0, utils_1.urlToPath)(mapped.source)
            : this.filePath
                ? (0, path_1.resolve)((0, path_1.dirname)(this.filePath), mapped.source).toString()
                : undefined;
        // ex: library publish with declarationMap but npmignore the original files
        if (!originalFilePath || !this.tsSys.fileExists(originalFilePath)) {
            return generatedPosition;
        }
        return {
            line: mapped.line - 1,
            character: mapped.column,
            uri: (0, utils_1.pathToUrl)(originalFilePath)
        };
    }
    initMapper() {
        const sourceMapUrl = tryGetSourceMappingURL(this.getLineOffsets(), this.getFullText());
        if (!sourceMapUrl) {
            return;
        }
        const match = sourceMapUrl.match(base64UrlRegExp);
        if (match) {
            const base64Json = match[1];
            if (!base64Json || !this.tsSys.base64decode) {
                return;
            }
            return this.initMapperByRawSourceMap(this.tsSys.base64decode(base64Json));
        }
        const tryingLocations = new Set([
            (0, path_1.resolve)((0, path_1.dirname)(this.filePath), sourceMapUrl),
            this.filePath + '.map'
        ]);
        for (const mapFilePath of tryingLocations) {
            if (!this.tsSys.fileExists(mapFilePath)) {
                continue;
            }
            const mapFileContent = this.tsSys.readFile(mapFilePath);
            if (mapFileContent) {
                return this.initMapperByRawSourceMap(mapFileContent);
            }
        }
        this.logFailedToResolveSourceMap("can't find valid sourcemap file");
    }
    initMapperByRawSourceMap(input) {
        const map = tryParseRawSourceMap(input);
        // don't support inline sourcemap because
        // it must be a file that editor can point to
        if (!map ||
            !map.mappings ||
            map.sourcesContent?.some((content) => typeof content === 'string')) {
            this.logFailedToResolveSourceMap('invalid or unsupported sourcemap');
            return;
        }
        return new trace_mapping_1.TraceMap(map);
    }
    logFailedToResolveSourceMap(...errors) {
        logger_1.Logger.debug(`Resolving declaration map for ${this.filePath} failed. `, ...errors);
    }
}
exports.DtsDocumentSnapshot = DtsDocumentSnapshot;
// https://github.com/microsoft/TypeScript/blob/1dc5b28b94b4a63f735a42d6497d538434d69b66/src/compiler/sourcemap.ts#L381
function tryGetSourceMappingURL(lineOffsets, text) {
    for (let index = lineOffsets.length - 1; index >= 0; index--) {
        const line = text.slice(lineOffsets[index], lineOffsets[index + 1]);
        const comment = sourceMapCommentRegExp.exec(line);
        if (comment) {
            return comment[1].trimEnd();
        }
        // If we see a non-whitespace/map comment-like line, break, to avoid scanning up the entire file
        else if (!line.match(whitespaceOrMapCommentRegExp)) {
            break;
        }
    }
}
// https://github.com/microsoft/TypeScript/blob/1dc5b28b94b4a63f735a42d6497d538434d69b66/src/compiler/sourcemap.ts#L402
function isRawSourceMap(x) {
    return (x !== null &&
        typeof x === 'object' &&
        x.version === 3 &&
        typeof x.file === 'string' &&
        typeof x.mappings === 'string' &&
        Array.isArray(x.sources) &&
        x.sources.every((source) => typeof source === 'string') &&
        (x.sourceRoot === undefined || x.sourceRoot === null || typeof x.sourceRoot === 'string') &&
        (x.sourcesContent === undefined ||
            x.sourcesContent === null ||
            (Array.isArray(x.sourcesContent) &&
                x.sourcesContent.every((content) => typeof content === 'string' || content === null))) &&
        (x.names === undefined ||
            x.names === null ||
            (Array.isArray(x.names) && x.names.every((name) => typeof name === 'string'))));
}
function tryParseRawSourceMap(text) {
    try {
        const parsed = JSON.parse(text);
        if (isRawSourceMap(parsed)) {
            return parsed;
        }
    }
    catch {
        // empty
    }
    return undefined;
}
//# sourceMappingURL=DocumentSnapshot.js.map