diff --git a/package.json b/package.json index f32f2d6..8519ece 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "author": "Asadbek Karimov ", "publisher": "asadbek", "icon": "img/logo.png", - "version": "0.0.3", + "version": "0.0.7", "license": "Apache-2.0", "bugs": { "url": "https://git.sheetjs.com/asadbek064/sheetjs-vscode-extension/issues" @@ -15,7 +15,7 @@ "url": "https://git.sheetjs.com/asadbek064/sheetjs-vscode-extension.git" }, "engines": { - "vscode": "^1.100.0" + "vscode": "^1.96.0" }, "categories": [ "Other" @@ -147,7 +147,7 @@ "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "20.x", - "@types/vscode": "^1.100.0", + "@types/vscode": "^1.96.0", "@typescript-eslint/eslint-plugin": "^8.31.1", "@typescript-eslint/parser": "^8.31.1", "@vscode/test-cli": "^0.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf201be..745b0cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,7 +15,7 @@ importers: specifier: 20.x version: 20.17.47 '@types/vscode': - specifier: ^1.100.0 + specifier: ^1.96.0 version: 1.100.0 '@typescript-eslint/eslint-plugin': specifier: ^8.31.1 @@ -144,8 +144,8 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@modelcontextprotocol/sdk@1.11.2': - resolution: {integrity: sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==} + '@modelcontextprotocol/sdk@1.11.3': + resolution: {integrity: sha512-rmOWVRUbUJD7iSvJugjUbFZshTAuJ48MXoZ80Osx1GM0K/H1w7rSEvmw8m6vdWxNASgtaHIhAgre4H/E9GJiYQ==} engines: {node: '>=18'} '@nodelib/fs.scandir@2.1.5': @@ -563,8 +563,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.152: - resolution: {integrity: sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==} + electron-to-chromium@1.5.154: + resolution: {integrity: sha512-G4VCFAyKbp1QJ+sWdXYIRYsPGvlV5sDACfCmoMFog3rjm1syLhI41WXm/swZypwCIWIm4IFLWzHY14joWMQ5Fw==} emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -1456,8 +1456,8 @@ packages: uglify-js: optional: true - terser@5.39.1: - resolution: {integrity: sha512-Mm6+uad0ZuDtcV8/4uOZQDQ8RuiC5Pu+iZRedJtF7yA/27sPL7d++In/AJKpWZlU3SYMPPkVfwetn6sgZ66pUA==} + terser@5.39.2: + resolution: {integrity: sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==} engines: {node: '>=10'} hasBin: true @@ -1726,7 +1726,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@modelcontextprotocol/sdk@1.11.2': + '@modelcontextprotocol/sdk@1.11.3': dependencies: content-type: 1.0.5 cors: 2.8.5 @@ -2065,7 +2065,7 @@ snapshots: browserslist@4.24.5: dependencies: caniuse-lite: 1.0.30001718 - electron-to-chromium: 1.5.152 + electron-to-chromium: 1.5.154 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.5) @@ -2211,7 +2211,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.152: {} + electron-to-chromium@1.5.154: {} emoji-regex@10.4.0: {} @@ -2271,7 +2271,7 @@ snapshots: '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 - '@modelcontextprotocol/sdk': 1.11.2 + '@modelcontextprotocol/sdk': 1.11.3 '@types/estree': 1.0.7 '@types/json-schema': 7.0.15 ajv: 6.12.6 @@ -3112,10 +3112,10 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.39.1 + terser: 5.39.2 webpack: 5.99.8(webpack-cli@6.0.1) - terser@5.39.1: + terser@5.39.2: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.14.1 diff --git a/src/excelEditorProvider.ts b/src/excelEditorProvider.ts index a42400d..4a6be53 100644 --- a/src/excelEditorProvider.ts +++ b/src/excelEditorProvider.ts @@ -11,13 +11,13 @@ export class ExcelEditorProvider implements vscode.CustomReadonlyEditorProvider< public static register(context: vscode.ExtensionContext): vscode.Disposable { return vscode.window.registerCustomEditorProvider( - 'excelViewer.spreadsheet', + 'excelViewer.spreadsheet', new ExcelEditorProvider(context), { webviewOptions: { retainContextWhenHidden: true } } // keep webview state when hidden ); } - constructor(private readonly context: vscode.ExtensionContext) {} + constructor(private readonly context: vscode.ExtensionContext) { } async openCustomDocument(uri: vscode.Uri): Promise { console.log(`Opening document: ${uri.fsPath}`); @@ -26,14 +26,14 @@ export class ExcelEditorProvider implements vscode.CustomReadonlyEditorProvider< async resolveCustomEditor(document: ExcelDocument, webviewPanel: vscode.WebviewPanel): Promise { console.log(`Resolving editor for: ${document.uri.fsPath}`); - + webviewPanel.webview.options = { enableScripts: true }; - + this.setLoadingView(webviewPanel); - + // small timeout to ensure the loading view is rendered before heavy processing begins await new Promise(resolve => setTimeout(resolve, 50)); - + try { // process file in a non-blocking way setImmediate(async () => { @@ -69,7 +69,7 @@ export class ExcelEditorProvider implements vscode.CustomReadonlyEditorProvider< // check if we have a cached workbook for this file let workbook: XLSX.WorkBook; const cacheKey = document.uri.toString(); - + if (this.workbookCache.has(cacheKey)) { console.log('Using cached workbook'); workbook = this.workbookCache.get(cacheKey)!; @@ -77,104 +77,104 @@ export class ExcelEditorProvider implements vscode.CustomReadonlyEditorProvider< } else { workbook = await this.loadWorkbook(document, webviewPanel); } - + // setup the initial view with just the sheet selector const sheetNames = workbook.SheetNames; this.setupWebviewContent(document, webviewPanel, workbook, sheetNames); } -private async loadWorkbook(document: ExcelDocument, webviewPanel: vscode.WebviewPanel): Promise { - this.updateLoadingProgress(webviewPanel, 'Reading file...'); - - try { - const data = await vscode.workspace.fs.readFile(document.uri); - console.log(`Read ${data.byteLength} bytes`); - - // very large files, update the progress message - if (data.byteLength > 5 * 1024 * 1024) { // 5MB - this.updateLoadingProgress(webviewPanel, 'Parsing large file...'); - } - - // get file extension to optimize parsing options - const fileExtension = document.uri.fsPath.split('.').pop()?.toLowerCase() || ''; - - // parse with SheetJS with options optimized for the file type - console.log(`Parsing ${fileExtension} file...`); - this.updateLoadingProgress(webviewPanel, `Parsing ${fileExtension} data...`); - - // timeout to ensure UI updates before heavy parsing - await new Promise(resolve => setTimeout(resolve, 10)); - - const options: XLSX.ParsingOptions = { - type: 'array', - cellStyles: true, - cellDates: true, - }; - - // specific format handling - if (fileExtension === 'csv') { - options.cellDates = false; - options.cellStyles = false; - } - - const workbook = XLSX.read(new Uint8Array(data), options); - - this.updateLoadingProgress(webviewPanel, 'Preparing view...'); - - // cache the workbook - const cacheKey = document.uri.toString(); - this.workbookCache.set(cacheKey, workbook); - - return workbook; - } catch (error) { - console.error(`Error reading file: ${error}`); - this.setErrorView(webviewPanel, error); - throw error; - } -} + private async loadWorkbook(document: ExcelDocument, webviewPanel: vscode.WebviewPanel): Promise { + this.updateLoadingProgress(webviewPanel, 'Reading file...'); - private setupWebviewContent( - document: ExcelDocument, - webviewPanel: vscode.WebviewPanel, - workbook: XLSX.WorkBook, - sheetNames: string[] -): void { - // exit early if there are no sheets - if (sheetNames.length === 0) { - webviewPanel.webview.html = getErrorViewHtml(new Error("No sheets or tables found in this file.")); - return; + try { + const data: Uint8Array = await vscode.workspace.fs.readFile(document.uri); + console.log(`Read ${data.byteLength} bytes`); + + // very large files, update the progress message + if (data.byteLength > 5 * 1024 * 1024) { // 5MB + this.updateLoadingProgress(webviewPanel, 'Parsing large file...'); + } + + // get file extension to optimize parsing options + const fileExtension = document.uri.fsPath.split('.').pop()?.toLowerCase() || ''; + + // parse with SheetJS with options optimized for the file type + console.log(`Parsing ${fileExtension} file...`); + this.updateLoadingProgress(webviewPanel, `Parsing ${fileExtension} data...`); + + // timeout to ensure UI updates before heavy parsing + await new Promise(resolve => setTimeout(resolve, 10)); + + const options: XLSX.ParsingOptions = { + type: 'array', + cellStyles: true, + cellDates: true, + }; + + // specific format handling + if (fileExtension === 'csv') { + options.cellDates = false; + options.cellStyles = false; + } + + const workbook: XLSX.WorkBook = XLSX.read(data, options); + + this.updateLoadingProgress(webviewPanel, 'Preparing view...'); + + // cache the workbook + const cacheKey = document.uri.toString(); + this.workbookCache.set(cacheKey, workbook); + + return workbook; + } catch (error) { + console.error(`Error reading file: ${error}`); + this.setErrorView(webviewPanel, error); + throw error; + } } - // create a dropdown for sheet selection - let sheetSelector = ` + private setupWebviewContent( + document: ExcelDocument, + webviewPanel: vscode.WebviewPanel, + workbook: XLSX.WorkBook, + sheetNames: string[] + ): void { + // exit early if there are no sheets + if (sheetNames.length === 0) { + webviewPanel.webview.html = getErrorViewHtml(new Error("No sheets or tables found in this file.")); + return; + } + + // create a dropdown for sheet selection + let sheetSelector = `
`; - - // setup up the HTML with JS to handle sheet switching - webviewPanel.webview.html = getExcelViewerHtml(sheetNames, sheetSelector); - - // handle messages from the webview - this.setupMessageHandlers(document, webviewPanel, workbook); -} + + // setup up the HTML with JS to handle sheet switching + webviewPanel.webview.html = getExcelViewerHtml(sheetNames, sheetSelector); + + // handle messages from the webview + this.setupMessageHandlers(document, webviewPanel, workbook); + } private setupMessageHandlers( - document: ExcelDocument, - webviewPanel: vscode.WebviewPanel, + document: ExcelDocument, + webviewPanel: vscode.WebviewPanel, workbook: XLSX.WorkBook ): void { const baseCacheKey = document.uri.toString(); - + webviewPanel.webview.onDidReceiveMessage(async message => { if (message.type === 'getSheetPage') { await this.handleGetSheetPage( @@ -195,35 +195,35 @@ private async loadWorkbook(document: ExcelDocument, webviewPanel: vscode.Webview ): Promise { const { sheetName, page, rowsPerPage, maxColumns } = message; const cacheKey = `${baseCacheKey}-${sheetName}-page-${page}`; - + this.updateLoadingProgress(webviewPanel, `Preparing page ${page + 1} of sheet: ${sheetName}`); - + // setTimeout to ensure loading message gets displayed setTimeout(async () => { try { let sheetHtml: string; let rangeInfo: any = null; - + // check if this page is already cached if (this.sheetCache.has(cacheKey)) { sheetHtml = this.sheetCache.get(cacheKey)!; } else { const sheet = workbook.Sheets[sheetName]; - + // get the range of the sheet (e.g., A1:Z100) const range = sheet['!ref'] || ''; rangeInfo = parseRange(range); - + if (!rangeInfo) { sheetHtml = '

No data in this sheet

'; } else { sheetHtml = this.processSheetPage(sheet, rangeInfo, page, rowsPerPage, maxColumns); - + // cache the result this.sheetCache.set(cacheKey, sheetHtml); } } - + // send the sheet data back to the webview webviewPanel.webview.postMessage({ type: 'sheetData', @@ -243,39 +243,39 @@ private async loadWorkbook(document: ExcelDocument, webviewPanel: vscode.Webview } private processSheetPage( - sheet: XLSX.WorkSheet, - rangeInfo: any, - page: number, - rowsPerPage: number, + sheet: XLSX.WorkSheet, + rangeInfo: any, + page: number, + rowsPerPage: number, maxColumns: number ): string { // calc the range for this page const pageStartRow = Math.min(rangeInfo.startRow + (page * rowsPerPage), rangeInfo.endRow); const pageEndRow = Math.min(pageStartRow + rowsPerPage - 1, rangeInfo.endRow); - + // limit columns if there are too many const effectiveEndColNum = Math.min(rangeInfo.endColNum, rangeInfo.startColNum + maxColumns - 1); const effectiveEndCol = numToColLetter(effectiveEndColNum); - + // create a new range for this page const pageRange = `${rangeInfo.startCol}${pageStartRow}:${effectiveEndCol}${pageEndRow}`; - + // create a new sheet with just this page's data const newPageSheet: XLSX.WorkSheet = { '!ref': pageRange }; - + // preserve important sheet properties if (sheet['!cols']) newPageSheet['!cols'] = sheet['!cols']; if (sheet['!rows']) newPageSheet['!rows'] = sheet['!rows']; if (sheet['!merges']) { // filter merges that are in this page's range newPageSheet['!merges'] = sheet['!merges'].filter(merge => { - return merge.s.r >= pageStartRow - 1 && - merge.e.r <= pageEndRow - 1 && - merge.s.c >= rangeInfo.startColNum - 1 && - merge.e.c <= effectiveEndColNum - 1; + return merge.s.r >= pageStartRow - 1 && + merge.e.r <= pageEndRow - 1 && + merge.s.c >= rangeInfo.startColNum - 1 && + merge.e.c <= effectiveEndColNum - 1; }); } - + // copy cells in range Object.keys(sheet).forEach(key => { if (key.charAt(0) !== '!') { @@ -283,16 +283,16 @@ private async loadWorkbook(document: ExcelDocument, webviewPanel: vscode.Webview const cellCol = key.replace(/[0-9]/g, ''); const cellRow = parseInt(key.replace(/[^0-9]/g, '')); const cellColNum = colLetterToNum(cellCol); - - if (cellRow >= pageStartRow && - cellRow <= pageEndRow && - cellColNum >= rangeInfo.startColNum && - cellColNum <= effectiveEndColNum) { + + if (cellRow >= pageStartRow && + cellRow <= pageEndRow && + cellColNum >= rangeInfo.startColNum && + cellColNum <= effectiveEndColNum) { newPageSheet[key] = sheet[key]; } } }); - + // convert to HTML return XLSX.utils.sheet_to_html(newPageSheet); }