"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;
const trace_mapping_1 = require("@jridgewell/trace-mapping");
const compiler_1 = require("svelte/compiler");
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 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");
/**
 * Initial version of snapshots.
 */
exports.INITIAL_VERSION = 0;
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, 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);
        }
        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) {
        let originalText = '';
        // 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.
        const normalizedPath = filePath.replace(/\\/g, '/');
        if (!normalizedPath.endsWith('node_modules/svelte/types/runtime/ambient.d.ts')) {
            originalText = tsSystem.readFile(filePath) || '';
        }
        if (normalizedPath.endsWith('svelte2tsx/svelte-shims.d.ts') ||
            normalizedPath.endsWith('svelte-check/dist/src/svelte-shims.d.ts')) {
            // 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) => normalizedPath.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) {
        var _a;
        const originalText = (_a = typescript_1.default.sys.readFile(filePath)) !== null && _a !== void 0 ? _a : '';
        return fromDocument(createDocument(filePath, originalText), options);
    }
    DocumentSnapshot.fromSvelteFilePath = fromSvelteFilePath;
})(DocumentSnapshot = exports.DocumentSnapshot || (exports.DocumentSnapshot = {}));
/**
 * Tries to preprocess the svelte document and convert the contents into better analyzable js/ts(x) content.
 */
function preprocessSvelteFile(document, options) {
    var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
    let tsxMap;
    let parserError = null;
    let nrPrependedLines = 0;
    let text = document.getText();
    let exportedNames = { has: () => false };
    let htmlAst;
    const scriptKind = [
        (0, utils_2.getScriptKindFromAttributes)((_b = (_a = document.scriptInfo) === null || _a === void 0 ? void 0 : _a.attributes) !== null && _b !== void 0 ? _b : {}),
        (0, utils_2.getScriptKindFromAttributes)((_d = (_c = document.moduleScriptInfo) === null || _c === void 0 ? void 0 : _c.attributes) !== null && _d !== void 0 ? _d : {})
    ].includes(typescript_1.default.ScriptKind.TSX)
        ? typescript_1.default.ScriptKind.TS
        : typescript_1.default.ScriptKind.JS;
    try {
        const tsx = (0, svelte2tsx_1.svelte2tsx)(text, {
            filename: (_e = document.getFilePath()) !== null && _e !== void 0 ? _e : undefined,
            isTsFile: scriptKind === typescript_1.default.ScriptKind.TS,
            mode: 'ts',
            typingsNamespace: options.typingsNamespace,
            emitOnTemplateError: options.transformOnTemplateError,
            namespace: (_g = (_f = document.config) === null || _f === void 0 ? void 0 : _f.compilerOptions) === null || _g === void 0 ? void 0 : _g.namespace,
            accessors: (_k = (_j = (_h = document.config) === null || _h === void 0 ? void 0 : _h.compilerOptions) === null || _j === void 0 ? void 0 : _j.accessors) !== null && _k !== void 0 ? _k : (_m = (_l = document.config) === null || _l === void 0 ? void 0 : _l.compilerOptions) === null || _m === void 0 ? void 0 : _m.customElement
        });
        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 === null || scriptInfo === void 0 ? void 0 : 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: ((_p = (_o = e.start) === null || _o === void 0 ? void 0 : _o.line) !== null && _p !== void 0 ? _p : 1) - 1,
            character: (_r = (_q = e.start) === null || _q === void 0 ? void 0 : _q.column) !== null && _r !== void 0 ? _r : 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, text, nrPrependedLines, exportedNames, tsxMap, htmlAst) {
        this.parent = parent;
        this.parserError = parserError;
        this.scriptKind = scriptKind;
        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;
    }
    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() {
        return undefined;
    }
    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;
        (0, compiler_1.walk)(this.htmlAst, {
            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;
                // Spread so the "parent" property isn't added to the original ast,
                // causing an infinite loop
                foundNode = { ...node };
                if (parent) {
                    foundNode.parent = parent;
                }
            }
        });
        return foundNode;
    }
    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;
    }
    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 {
    constructor(version, filePath, text) {
        super((0, utils_1.pathToUrl)(filePath));
        this.version = version;
        this.filePath = filePath;
        this.text = text;
        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.adjustText();
    }
    getText(start, end) {
        return this.text.substring(start, end);
    }
    getLength() {
        return this.text.length;
    }
    getFullText() {
        return this.text;
    }
    getChangeRange() {
        return undefined;
    }
    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) {
        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 = this.originalText.length;
            }
            this.originalText =
                this.originalText.slice(0, start) + change.text + this.originalText.slice(end);
        }
        this.adjustText();
        this.version++;
        this.lineOffsets = undefined;
        this.internalLineOffsets = undefined;
    }
    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() {
        var _a, _b, _c, _d;
        const result = svelte2tsx_1.internalHelpers.upsertKitFile(typescript_1.default, this.filePath, {
            clientHooksPath: this.clientHooksPath,
            paramsPath: this.paramsPath,
            serverHooksPath: this.serverHooksPath
        }, () => this.createSource(), utils_3.surroundWithIgnoreComments);
        if (!result) {
            this.kitFile = false;
            this.addedCode = [];
            this.text = this.originalText;
            return;
        }
        if (!this.kitFile) {
            const files = (_b = (_a = configLoader_1.configLoader.getConfig(this.filePath)) === null || _a === void 0 ? void 0 : _a.kit) === null || _b === void 0 ? void 0 : _b.files;
            if (files) {
                this.paramsPath || (this.paramsPath = files.params);
                this.serverHooksPath || (this.serverHooksPath = (_c = files.hooks) === null || _c === void 0 ? void 0 : _c.server);
                this.clientHooksPath || (this.clientHooksPath = (_d = files.hooks) === null || _d === void 0 ? void 0 : _d.client);
            }
        }
        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,
            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) {
        var _a;
        const map = tryParseRawSourceMap(input);
        // don't support inline sourcemap because
        // it must be a file that editor can point to
        if (!map ||
            !map.mappings ||
            ((_a = map.sourcesContent) === null || _a === void 0 ? void 0 : _a.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