first commit
This commit is contained in:
commit
7930d45ec2
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
out
|
||||
dist
|
||||
node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
1
.npmrc
Normal file
1
.npmrc
Normal file
@ -0,0 +1 @@
|
||||
enable-pre-post-scripts = true
|
5
.vscode-test.mjs
Normal file
5
.vscode-test.mjs
Normal file
@ -0,0 +1,5 @@
|
||||
import { defineConfig } from '@vscode/test-cli';
|
||||
|
||||
export default defineConfig({
|
||||
files: 'out/test/**/*.test.js',
|
||||
});
|
14
.vscodeignore
Normal file
14
.vscodeignore
Normal file
@ -0,0 +1,14 @@
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
out/**
|
||||
node_modules/**
|
||||
src/**
|
||||
.gitignore
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
vsc-extension-quickstart.md
|
||||
**/tsconfig.json
|
||||
**/eslint.config.mjs
|
||||
**/*.map
|
||||
**/*.ts
|
||||
**/.vscode-test.*
|
9
CHANGELOG.md
Normal file
9
CHANGELOG.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to the "sheetjs-demo" extension will be documented in this file.
|
||||
|
||||
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Initial release
|
201
LICENSE
Normal file
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (C) 2014-present SheetJS LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
71
README.md
Normal file
71
README.md
Normal file
@ -0,0 +1,71 @@
|
||||
# sheetjs-demo README
|
||||
|
||||
This is the README for your extension "sheetjs-demo". After writing up a brief description, we recommend including the following sections.
|
||||
|
||||
## Features
|
||||
|
||||
Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file.
|
||||
|
||||
For example if there is an image subfolder under your extension project workspace:
|
||||
|
||||
\!\[feature X\]\(images/feature-x.png\)
|
||||
|
||||
> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow.
|
||||
|
||||
## Requirements
|
||||
|
||||
If you have any requirements or dependencies, add a section describing those and how to install and configure them.
|
||||
|
||||
## Extension Settings
|
||||
|
||||
Include if your extension adds any VS Code settings through the `contributes.configuration` extension point.
|
||||
|
||||
For example:
|
||||
|
||||
This extension contributes the following settings:
|
||||
|
||||
* `myExtension.enable`: Enable/disable this extension.
|
||||
* `myExtension.thing`: Set to `blah` to do something.
|
||||
|
||||
## Known Issues
|
||||
|
||||
Calling out known issues can help limit users opening duplicate issues against your extension.
|
||||
|
||||
## Release Notes
|
||||
|
||||
Users appreciate release notes as you update your extension.
|
||||
|
||||
### 1.0.0
|
||||
|
||||
Initial release of ...
|
||||
|
||||
### 1.0.1
|
||||
|
||||
Fixed issue #.
|
||||
|
||||
### 1.1.0
|
||||
|
||||
Added features X, Y, and Z.
|
||||
|
||||
---
|
||||
|
||||
## Following extension guidelines
|
||||
|
||||
Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension.
|
||||
|
||||
* [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)
|
||||
|
||||
## Working with Markdown
|
||||
|
||||
You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts:
|
||||
|
||||
* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux).
|
||||
* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux).
|
||||
* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets.
|
||||
|
||||
## For more information
|
||||
|
||||
* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown)
|
||||
* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/)
|
||||
|
||||
**Enjoy!**
|
28
eslint.config.mjs
Normal file
28
eslint.config.mjs
Normal file
@ -0,0 +1,28 @@
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
|
||||
export default [{
|
||||
files: ["**/*.ts"],
|
||||
}, {
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
},
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/naming-convention": ["warn", {
|
||||
selector: "import",
|
||||
format: ["camelCase", "PascalCase"],
|
||||
}],
|
||||
|
||||
curly: "warn",
|
||||
eqeqeq: "warn",
|
||||
"no-throw-literal": "warn",
|
||||
semi: "warn",
|
||||
},
|
||||
}];
|
BIN
image.png
Normal file
BIN
image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 142 KiB |
92
package.json
Normal file
92
package.json
Normal file
@ -0,0 +1,92 @@
|
||||
{
|
||||
"name": "sheetjs-demo",
|
||||
"displayName": "Spreadsheet Viewer",
|
||||
"description": "View spreadsheets in various formats including XLSX, XLS, CSV, ODS and many more",
|
||||
"version": "0.0.1",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"vscode": "^1.100.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"keywords": [
|
||||
"CSV",
|
||||
"Excel",
|
||||
"spreadsheet",
|
||||
"viewer",
|
||||
"vscode"
|
||||
],
|
||||
"activationEvents": [],
|
||||
"main": "./dist/extension.js",
|
||||
"contributes": {
|
||||
"customEditors": [
|
||||
{
|
||||
"viewType": "excelViewer.spreadsheet",
|
||||
"displayName": "SheetJS Viewer",
|
||||
"selector": [
|
||||
{ "filenamePattern": "*.xlsx" },
|
||||
{ "filenamePattern": "*.xlsm" },
|
||||
{ "filenamePattern": "*.xlsb" },
|
||||
{ "filenamePattern": "*.xls" },
|
||||
{ "filenamePattern": "*.xlw" },
|
||||
{ "filenamePattern": "*.xlr" },
|
||||
{ "filenamePattern": "*.csv" },
|
||||
{ "filenamePattern": "*.dif" },
|
||||
{ "filenamePattern": "*.slk" },
|
||||
{ "filenamePattern": "*.sylk" },
|
||||
{ "filenamePattern": "*.prn" },
|
||||
{ "filenamePattern": "*.numbers" },
|
||||
{ "filenamePattern": "*.et" },
|
||||
{ "filenamePattern": "*.ods" },
|
||||
{ "filenamePattern": "*.fods" },
|
||||
{ "filenamePattern": "*.uos" },
|
||||
{ "filenamePattern": "*.dbf" },
|
||||
{ "filenamePattern": "*.wk1" },
|
||||
{ "filenamePattern": "*.wk3" },
|
||||
{ "filenamePattern": "*.wks" },
|
||||
{ "filenamePattern": "*.wk2" },
|
||||
{ "filenamePattern": "*.wk4" },
|
||||
{ "filenamePattern": "*.123" },
|
||||
{ "filenamePattern": "*.wq1" },
|
||||
{ "filenamePattern": "*.wq2" },
|
||||
{ "filenamePattern": "*.wb1" },
|
||||
{ "filenamePattern": "*.wb2" },
|
||||
{ "filenamePattern": "*.wb3" },
|
||||
{ "filenamePattern": "*.qpw" },
|
||||
{ "filenamePattern": "*.xlr" },
|
||||
{ "filenamePattern": "*.eth" }
|
||||
],
|
||||
"priority": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "pnpm run package",
|
||||
"compile": "webpack",
|
||||
"watch": "webpack --watch",
|
||||
"package": "webpack --mode production --devtool hidden-source-map",
|
||||
"compile-tests": "tsc -p . --outDir out",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "pnpm run compile-tests && pnpm run compile && pnpm run lint",
|
||||
"lint": "eslint src",
|
||||
"test": "vscode-test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "20.x",
|
||||
"@types/vscode": "^1.100.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.1",
|
||||
"@typescript-eslint/parser": "^8.31.1",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
"eslint": "^9.25.1",
|
||||
"ts-loader": "^9.5.2",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.7",
|
||||
"webpack-cli": "^6.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
}
|
||||
}
|
3316
pnpm-lock.yaml
Normal file
3316
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
6
src/excelDocument.ts
Normal file
6
src/excelDocument.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class ExcelDocument implements vscode.CustomDocument {
|
||||
constructor(public readonly uri: vscode.Uri) {}
|
||||
dispose() {}
|
||||
}
|
299
src/excelEditorProvider.ts
Normal file
299
src/excelEditorProvider.ts
Normal file
@ -0,0 +1,299 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { ExcelDocument } from './excelDocument';
|
||||
import { getLoadingViewHtml, getErrorViewHtml, getExcelViewerHtml } from './webviewContent';
|
||||
import { parseRange, colLetterToNum, numToColLetter } from './excelUtils';
|
||||
|
||||
export class ExcelEditorProvider implements vscode.CustomReadonlyEditorProvider<ExcelDocument> {
|
||||
// cache workbooks in memory to avoid re-parsing
|
||||
private workbookCache = new Map<string, XLSX.WorkBook>();
|
||||
private sheetCache = new Map<string, string>();
|
||||
|
||||
public static register(context: vscode.ExtensionContext): vscode.Disposable {
|
||||
return vscode.window.registerCustomEditorProvider(
|
||||
'excelViewer.spreadsheet',
|
||||
new ExcelEditorProvider(context),
|
||||
{ webviewOptions: { retainContextWhenHidden: true } } // keep webview state when hidden
|
||||
);
|
||||
}
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {}
|
||||
|
||||
async openCustomDocument(uri: vscode.Uri): Promise<ExcelDocument> {
|
||||
console.log(`Opening document: ${uri.fsPath}`);
|
||||
return new ExcelDocument(uri);
|
||||
}
|
||||
|
||||
async resolveCustomEditor(document: ExcelDocument, webviewPanel: vscode.WebviewPanel): Promise<void> {
|
||||
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 () => {
|
||||
try {
|
||||
await this.processExcelFile(document, webviewPanel);
|
||||
} catch (error) {
|
||||
console.error('Error processing file:', error);
|
||||
this.setErrorView(webviewPanel, error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error setting up Excel viewer:', error);
|
||||
this.setErrorView(webviewPanel, error);
|
||||
}
|
||||
}
|
||||
|
||||
private setLoadingView(webviewPanel: vscode.WebviewPanel): void {
|
||||
webviewPanel.webview.html = getLoadingViewHtml();
|
||||
}
|
||||
|
||||
private setErrorView(webviewPanel: vscode.WebviewPanel, error: any): void {
|
||||
webviewPanel.webview.html = getErrorViewHtml(error);
|
||||
}
|
||||
|
||||
private updateLoadingProgress(webviewPanel: vscode.WebviewPanel, message: string): void {
|
||||
webviewPanel.webview.postMessage({
|
||||
type: 'loadingProgress',
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
private async processExcelFile(document: ExcelDocument, webviewPanel: vscode.WebviewPanel): Promise<void> {
|
||||
// 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)!;
|
||||
this.updateLoadingProgress(webviewPanel, 'Using cached workbook...');
|
||||
} 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<XLSX.WorkBook> {
|
||||
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 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 = `
|
||||
<div class="sheet-selector">
|
||||
<label for="sheet-select">Select worksheet: </label>
|
||||
<select id="sheet-select">
|
||||
`;
|
||||
|
||||
sheetNames.forEach((name, index) => {
|
||||
sheetSelector += `<option value="${index}">${name}</option>`;
|
||||
});
|
||||
|
||||
sheetSelector += `
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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,
|
||||
workbook: XLSX.WorkBook
|
||||
): void {
|
||||
const baseCacheKey = document.uri.toString();
|
||||
|
||||
webviewPanel.webview.onDidReceiveMessage(async message => {
|
||||
if (message.type === 'getSheetPage') {
|
||||
await this.handleGetSheetPage(
|
||||
baseCacheKey,
|
||||
workbook,
|
||||
webviewPanel,
|
||||
message
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleGetSheetPage(
|
||||
baseCacheKey: string,
|
||||
workbook: XLSX.WorkBook,
|
||||
webviewPanel: vscode.WebviewPanel,
|
||||
message: any
|
||||
): Promise<void> {
|
||||
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 = '<p>No data in this sheet</p>';
|
||||
} 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',
|
||||
sheetName: sheetName,
|
||||
page: page,
|
||||
html: sheetHtml,
|
||||
range: rangeInfo
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error processing sheet ${sheetName}:`, error);
|
||||
webviewPanel.webview.postMessage({
|
||||
type: 'error',
|
||||
message: `Error loading sheet: ${error}`
|
||||
});
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
private processSheetPage(
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// copy cells in range
|
||||
Object.keys(sheet).forEach(key => {
|
||||
if (key.charAt(0) !== '!') {
|
||||
// check if cell is in our page range
|
||||
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) {
|
||||
newPageSheet[key] = sheet[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// convert to HTML
|
||||
return XLSX.utils.sheet_to_html(newPageSheet);
|
||||
}
|
||||
}
|
51
src/excelUtils.ts
Normal file
51
src/excelUtils.ts
Normal file
@ -0,0 +1,51 @@
|
||||
// parse range like A1:Z100 and return information about dimensions
|
||||
export function parseRange(rangeStr: string) {
|
||||
if (!rangeStr || !rangeStr.includes(':')) return null;
|
||||
|
||||
const parts = rangeStr.split(':');
|
||||
const startCell = parts[0];
|
||||
const endCell = parts[1];
|
||||
|
||||
// extract column letters and row numbers
|
||||
const startCol = startCell.replace(/[0-9]/g, '');
|
||||
const endCol = endCell.replace(/[0-9]/g, '');
|
||||
const startRow = parseInt(startCell.replace(/[^0-9]/g, ''));
|
||||
const endRow = parseInt(endCell.replace(/[^0-9]/g, ''));
|
||||
|
||||
// calc dimensions
|
||||
const startColNum = colLetterToNum(startCol);
|
||||
const endColNum = colLetterToNum(endCol);
|
||||
const totalRows = endRow - startRow + 1;
|
||||
const totalCols = endColNum - startColNum + 1;
|
||||
|
||||
return {
|
||||
startRow,
|
||||
endRow,
|
||||
startCol,
|
||||
endCol,
|
||||
startColNum,
|
||||
endColNum,
|
||||
totalRows,
|
||||
totalCols
|
||||
};
|
||||
}
|
||||
|
||||
// (A=1, B=2, etc.)
|
||||
export function colLetterToNum(col: string): number {
|
||||
let result = 0;
|
||||
for (let i = 0; i < col.length; i++) {
|
||||
result = result * 26 + (col.charCodeAt(i) - 64);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// (1=A, 2=B, etc.)
|
||||
export function numToColLetter(num: number): string {
|
||||
let result = '';
|
||||
while (num > 0) {
|
||||
const modulo = (num - 1) % 26;
|
||||
result = String.fromCharCode(65 + modulo) + result;
|
||||
num = Math.floor((num - modulo) / 26);
|
||||
}
|
||||
return result || 'A';
|
||||
}
|
14
src/extension.ts
Normal file
14
src/extension.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { ExcelEditorProvider } from './excelEditorProvider';
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log('Excel Viewer extension activating...');
|
||||
|
||||
const provider = ExcelEditorProvider.register(context);
|
||||
context.subscriptions.push(provider);
|
||||
|
||||
console.log('Excel Viewer extension is now active');
|
||||
vscode.window.showInformationMessage('Excel Viewer is ready!');
|
||||
}
|
||||
|
||||
export function deactivate() {}
|
15
src/test/extension.test.ts
Normal file
15
src/test/extension.test.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import * as assert from 'assert';
|
||||
|
||||
// You can import and use all API from the 'vscode' module
|
||||
// as well as import your extension to test it
|
||||
import * as vscode from 'vscode';
|
||||
// import * as myExtension from '../../extension';
|
||||
|
||||
suite('Extension Test Suite', () => {
|
||||
vscode.window.showInformationMessage('Start all tests.');
|
||||
|
||||
test('Sample test', () => {
|
||||
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
|
||||
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
|
||||
});
|
||||
});
|
441
src/webviewContent.ts
Normal file
441
src/webviewContent.ts
Normal file
@ -0,0 +1,441 @@
|
||||
const commonStyles = `
|
||||
body {
|
||||
font-family: var(--vscode-font-family);
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-foreground);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const loadingStyles = `
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
.loader-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.loader {
|
||||
border: 6px solid var(--vscode-panel-border);
|
||||
border-radius: 50%;
|
||||
border-top: 6px solid var(--vscode-button-background);
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
animation: spin 1.5s linear infinite;
|
||||
margin: 30px auto;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
h2 { margin-bottom: 20px; }
|
||||
.message { margin-top: 20px; max-width: 80%; }
|
||||
`;
|
||||
|
||||
const errorStyles = `
|
||||
body { padding: 20px; }
|
||||
.error-container {
|
||||
border: 1px solid var(--vscode-inputValidation-errorBorder);
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
h1 { color: var(--vscode-errorForeground); }
|
||||
pre {
|
||||
background-color: var(--vscode-editor-background);
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const excelViewerStyles = `
|
||||
body {
|
||||
padding: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
.sheet-selector {
|
||||
flex: 0 0 auto;
|
||||
padding: 10px 20px;
|
||||
margin-bottom: 0;
|
||||
background-color: var(--vscode-editor-background);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
.sheet-content {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin-top: 15px;
|
||||
table-layout: fixed;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
padding: 6px;
|
||||
text-align: left;
|
||||
min-width: 80px;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
th {
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
.status-bar {
|
||||
flex: 0 0 auto;
|
||||
padding: 5px 20px;
|
||||
background-color: var(--vscode-editor-background);
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
select {
|
||||
padding: 5px;
|
||||
background-color: var(--vscode-dropdown-background);
|
||||
color: var(--vscode-dropdown-foreground);
|
||||
border: 1px solid var(--vscode-dropdown-border);
|
||||
border-radius: 2px;
|
||||
min-width: 200px;
|
||||
}
|
||||
label { margin-right: 10px; }
|
||||
.loader {
|
||||
border: 4px solid var(--vscode-panel-border);
|
||||
border-radius: 50%;
|
||||
border-top: 4px solid var(--vscode-button-background);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.loading-indicator {
|
||||
display: none;
|
||||
align-items: center;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.loading-text { margin-left: 8px; }
|
||||
.controls { display: flex; align-items: center; }
|
||||
.pagination {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
button {
|
||||
background-color: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
padding: 4px 10px;
|
||||
margin: 0 5px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.range-indicator {
|
||||
margin: 0 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.nav-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const loadingScript = `
|
||||
// set up message listener to update loading status
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
if (message && message.type === 'loadingProgress') {
|
||||
document.getElementById('loading-status').textContent = message.message;
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
function getExcelViewerScript(sheetNames: string[]) {
|
||||
return `
|
||||
// config
|
||||
const config = {
|
||||
rowsPerPage: 500,
|
||||
maxColumns: 100
|
||||
};
|
||||
|
||||
// Store sheet names and state
|
||||
const sheetNames = ${JSON.stringify(sheetNames)};
|
||||
const vscode = acquireVsCodeApi();
|
||||
const cache = {};
|
||||
let currentPage = 0;
|
||||
let currentSheetName = '';
|
||||
let currentSheetRange = null;
|
||||
|
||||
// DOM elements
|
||||
const currentSheetElement = document.getElementById('current-sheet');
|
||||
const prevButton = document.getElementById('prev-page');
|
||||
const nextButton = document.getElementById('next-page');
|
||||
const rangeIndicator = document.getElementById('range-indicator');
|
||||
const statusBar = document.getElementById('status-bar');
|
||||
const sheetSelector = document.getElementById('sheet-select');
|
||||
|
||||
// loading indicator functions
|
||||
function showLoading(message = "Loading...") {
|
||||
const indicator = document.getElementById('loading-indicator');
|
||||
const loadingText = indicator.querySelector('.loading-text');
|
||||
loadingText.textContent = message;
|
||||
indicator.style.display = 'flex';
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById('loading-indicator').style.display = 'none';
|
||||
}
|
||||
|
||||
function parseSheetRange(rangeStr) {
|
||||
if (!rangeStr || !rangeStr.includes(':')) return null;
|
||||
|
||||
const parts = rangeStr.split(':');
|
||||
const startCell = parts[0];
|
||||
const endCell = parts[1];
|
||||
|
||||
// extract column letters and row numbers
|
||||
const startCol = startCell.replace(/[0-9]/g, '');
|
||||
const endCol = endCell.replace(/[0-9]/g, '');
|
||||
const startRow = parseInt(startCell.replace(/[^0-9]/g, ''));
|
||||
const endRow = parseInt(endCell.replace(/[^0-9]/g, ''));
|
||||
|
||||
// calc dimensions
|
||||
const totalRows = endRow - startRow + 1;
|
||||
const startColNum = colLetterToNum(startCol);
|
||||
const endColNum = colLetterToNum(endCol);
|
||||
const totalCols = endColNum - startColNum + 1;
|
||||
|
||||
return {
|
||||
startRow, endRow, startCol, endCol,
|
||||
startColNum, endColNum, totalRows, totalCols
|
||||
};
|
||||
}
|
||||
|
||||
function colLetterToNum(col) {
|
||||
let result = 0;
|
||||
for (let i = 0; i < col.length; i++) {
|
||||
result = result * 26 + (col.charCodeAt(i) - 64);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function numToColLetter(num) {
|
||||
let result = '';
|
||||
while (num > 0) {
|
||||
const modulo = (num - 1) % 26;
|
||||
result = String.fromCharCode(65 + modulo) + result;
|
||||
num = Math.floor((num - modulo) / 26);
|
||||
}
|
||||
return result || 'A';
|
||||
}
|
||||
|
||||
// UI update functions
|
||||
function updatePaginationControls(page, totalRows) {
|
||||
const totalPages = Math.ceil(totalRows / config.rowsPerPage);
|
||||
prevButton.disabled = page <= 0;
|
||||
nextButton.disabled = page >= totalPages - 1;
|
||||
|
||||
const startRow = page * config.rowsPerPage + 1;
|
||||
const endRow = Math.min((page + 1) * config.rowsPerPage, totalRows);
|
||||
|
||||
rangeIndicator.textContent = \`Rows \${startRow}-\${endRow} of \${totalRows}\`;
|
||||
}
|
||||
|
||||
function updateStatusBar(sheetName, range) {
|
||||
if (!range) {
|
||||
statusBar.textContent = \`Sheet: \${sheetName}\`;
|
||||
return;
|
||||
}
|
||||
|
||||
statusBar.textContent = \`Sheet: \${sheetName} | Total: \${range.totalRows} rows × \${range.totalCols} columns\`;
|
||||
}
|
||||
|
||||
// Sheet loading function
|
||||
function loadSheetPage(sheetName, page) {
|
||||
currentPage = page;
|
||||
currentSheetName = sheetName;
|
||||
|
||||
showLoading(\`Loading page \${page + 1} of sheet \${sheetName}...\`);
|
||||
|
||||
vscode.postMessage({
|
||||
type: 'getSheetPage',
|
||||
sheetName: sheetName,
|
||||
page: page,
|
||||
rowsPerPage: config.rowsPerPage,
|
||||
maxColumns: config.maxColumns
|
||||
});
|
||||
}
|
||||
|
||||
// Event handling
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
|
||||
if (message.type === 'sheetData') {
|
||||
// add the sheet to the cache
|
||||
const cacheKey = \`\${message.sheetName}-page-\${message.page}\`;
|
||||
cache[cacheKey] = message.html;
|
||||
|
||||
// update range info if provided
|
||||
if (message.range) {
|
||||
currentSheetRange = message.range;
|
||||
updatePaginationControls(message.page, message.range.totalRows);
|
||||
updateStatusBar(message.sheetName, message.range);
|
||||
}
|
||||
|
||||
// update the view if this is still the current sheet and page
|
||||
if (currentSheetName === message.sheetName && currentPage === message.page) {
|
||||
currentSheetElement.innerHTML = \`<h2>\${message.sheetName}</h2>\${message.html}\`;
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
else if (message.type === 'loadingProgress') {
|
||||
showLoading(message.message);
|
||||
}
|
||||
else if (message.type === 'error') {
|
||||
currentSheetElement.innerHTML = \`<h2>Error</h2><p>\${message.message}</p>\`;
|
||||
hideLoading();
|
||||
}
|
||||
});
|
||||
|
||||
// UI event listeners
|
||||
sheetSelector.addEventListener('change', function(e) {
|
||||
const selectedIndex = parseInt(e.target.value);
|
||||
const selectedSheet = sheetNames[selectedIndex];
|
||||
|
||||
// reset page to 0 when changing sheets
|
||||
currentPage = 0;
|
||||
loadSheetPage(selectedSheet, 0);
|
||||
});
|
||||
|
||||
prevButton.addEventListener('click', function() {
|
||||
if (currentPage > 0) {
|
||||
loadSheetPage(currentSheetName, currentPage - 1);
|
||||
}
|
||||
});
|
||||
|
||||
nextButton.addEventListener('click', function() {
|
||||
loadSheetPage(currentSheetName, currentPage + 1);
|
||||
});
|
||||
|
||||
// init with first sheet
|
||||
if (sheetNames.length > 0) {
|
||||
loadSheetPage(sheetNames[0], 0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
// create HTML document with given components
|
||||
function createHtmlDocument(styles: string, body: string, script: string = ''): string {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
${commonStyles}
|
||||
${styles}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${body}
|
||||
|
||||
<script>
|
||||
${script}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
// HTML for loading view
|
||||
export function getLoadingViewHtml(): string {
|
||||
const body = `
|
||||
<div class="loader-container">
|
||||
<h2>Loading File</h2>
|
||||
<div class="loader"></div>
|
||||
<p class="message">Please wait while we process the file. Large files may take a moment to load...</p>
|
||||
<p id="loading-status">Initializing...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return createHtmlDocument(loadingStyles, body, loadingScript);
|
||||
}
|
||||
|
||||
// HTML for error view
|
||||
export function getErrorViewHtml(error: any): string {
|
||||
const body = `
|
||||
<h1>Error Opening File</h1>
|
||||
<p>An error occurred while trying to open the file:</p>
|
||||
<div class="error-container">
|
||||
<pre>${error.toString()}</pre>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return createHtmlDocument(errorStyles, body);
|
||||
}
|
||||
|
||||
// HTML for Excel viewer
|
||||
export function getExcelViewerHtml(sheetNames: string[], sheetSelector: string): string {
|
||||
const body = `
|
||||
<div class="container">
|
||||
<div class="sheet-selector">
|
||||
<div class="nav-controls">
|
||||
<div class="controls">
|
||||
${sheetSelector}
|
||||
<div id="loading-indicator" class="loading-indicator">
|
||||
<div class="loader"></div>
|
||||
<span class="loading-text">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<button id="prev-page" disabled>← Previous</button>
|
||||
<span class="range-indicator" id="range-indicator">Loading...</span>
|
||||
<button id="next-page">Next →</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sheet-content">
|
||||
<div id="current-sheet">
|
||||
<p>Loading sheet data...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-bar" id="status-bar">
|
||||
Ready
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return createHtmlDocument(excelViewerStyles, body, getExcelViewerScript(sheetNames));
|
||||
}
|
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"target": "ES2022",
|
||||
"lib": [
|
||||
"ES2022"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true, /* enable all strict type-checking options */
|
||||
/* Additional Checks */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
}
|
||||
}
|
48
vsc-extension-quickstart.md
Normal file
48
vsc-extension-quickstart.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Welcome to your VS Code Extension
|
||||
|
||||
## What's in the folder
|
||||
|
||||
* This folder contains all of the files necessary for your extension.
|
||||
* `package.json` - this is the manifest file in which you declare your extension and command.
|
||||
* The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
|
||||
* `src/extension.ts` - this is the main file where you will provide the implementation of your command.
|
||||
* The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
|
||||
* We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
|
||||
|
||||
## Setup
|
||||
|
||||
* install the recommended extensions (amodio.tsl-problem-matcher, ms-vscode.extension-test-runner, and dbaeumer.vscode-eslint)
|
||||
|
||||
|
||||
## Get up and running straight away
|
||||
|
||||
* Press `F5` to open a new window with your extension loaded.
|
||||
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
|
||||
* Set breakpoints in your code inside `src/extension.ts` to debug your extension.
|
||||
* Find output from your extension in the debug console.
|
||||
|
||||
## Make changes
|
||||
|
||||
* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`.
|
||||
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
|
||||
|
||||
|
||||
## Explore the API
|
||||
|
||||
* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
|
||||
|
||||
## Run tests
|
||||
|
||||
* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner)
|
||||
* Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered.
|
||||
* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A`
|
||||
* See the output of the test result in the Test Results view.
|
||||
* Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder.
|
||||
* The provided test runner will only consider files matching the name pattern `**.test.ts`.
|
||||
* You can create folders inside the `test` folder to structure your tests any way you want.
|
||||
|
||||
## Go further
|
||||
|
||||
* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension).
|
||||
* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace.
|
||||
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).
|
48
webpack.config.js
Normal file
48
webpack.config.js
Normal file
@ -0,0 +1,48 @@
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
//@ts-check
|
||||
/** @typedef {import('webpack').Configuration} WebpackConfig **/
|
||||
|
||||
/** @type WebpackConfig */
|
||||
const extensionConfig = {
|
||||
target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
|
||||
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
|
||||
|
||||
entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
|
||||
output: {
|
||||
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'extension.js',
|
||||
libraryTarget: 'commonjs2'
|
||||
},
|
||||
externals: {
|
||||
vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
|
||||
// modules added here also need to be added in the .vscodeignore file
|
||||
},
|
||||
resolve: {
|
||||
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
|
||||
extensions: ['.ts', '.js']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
devtool: 'nosources-source-map',
|
||||
infrastructureLogging: {
|
||||
level: "log", // enables logging required for problem matchers
|
||||
},
|
||||
};
|
||||
module.exports = [ extensionConfig ];
|
Loading…
Reference in New Issue
Block a user