Compare commits
10
Commits
2bc47f1d1b
...
ed33df2c16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed33df2c16 | ||
|
|
c4e6c7cd83 | ||
|
|
3087896926 | ||
|
|
2f3b15cd69 | ||
|
|
c6851c8951 | ||
|
|
cd70bcd23e | ||
|
|
e0687c649b | ||
|
|
6a8679649d | ||
|
|
dd1e0aed90 | ||
|
|
07cc5a2435 |
@@ -1,9 +0,0 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to the "vsctags" 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
|
||||
+5
-2
@@ -23,6 +23,10 @@
|
||||
{
|
||||
"command": "vsctags.showLog",
|
||||
"title": "vsctags: Show Log"
|
||||
},
|
||||
{
|
||||
"command": "vsctags.showWelcome",
|
||||
"title": "vsctags: Welcome"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -31,8 +35,7 @@
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"pretest": "npm run compile && npm run lint",
|
||||
"lint": "eslint src",
|
||||
"test": "vscode-test"
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.109.0",
|
||||
|
||||
+362
-110
@@ -1,6 +1,5 @@
|
||||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs/promises";
|
||||
import { createReadStream } from "fs";
|
||||
import * as readline from "readline";
|
||||
|
||||
@@ -50,9 +49,14 @@ function extractSearchText(pattern: string): string {
|
||||
* Parse a ctags file using streaming readline.
|
||||
* Avoids loading the entire file into memory as a single string.
|
||||
*/
|
||||
function streamParseCtagsFile(tagsPath: string): Promise<CtagsEntry[]> {
|
||||
function streamParseCtagsFile(
|
||||
tagsPath: string,
|
||||
onProgress?: (count: number) => void,
|
||||
): Promise<CtagsEntry[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const entries: CtagsEntry[] = [];
|
||||
let progressCounter = 0;
|
||||
const progressInterval = 50000; // report every 50K entries
|
||||
const rl = readline.createInterface({
|
||||
input: createReadStream(tagsPath, { encoding: "utf-8" }),
|
||||
crlfDelay: Infinity,
|
||||
@@ -118,9 +122,17 @@ function streamParseCtagsFile(tagsPath: string): Promise<CtagsEntry[]> {
|
||||
lineNumber,
|
||||
scope,
|
||||
});
|
||||
|
||||
progressCounter++;
|
||||
if (onProgress && progressCounter % progressInterval === 0) {
|
||||
onProgress(progressCounter);
|
||||
}
|
||||
});
|
||||
|
||||
rl.on("close", () => resolve(entries));
|
||||
rl.on("close", () => {
|
||||
if (onProgress) { onProgress(entries.length); }
|
||||
resolve(entries);
|
||||
});
|
||||
rl.on("error", (err: Error) => reject(err));
|
||||
});
|
||||
}
|
||||
@@ -224,21 +236,31 @@ function substringSearch(db: TagDatabase, query: string, limit: number): CtagsEn
|
||||
return results;
|
||||
}
|
||||
|
||||
// ---- URI cache ----
|
||||
|
||||
/** Cache of file URIs to avoid redundant Uri.joinPath calls */
|
||||
let fileUriCache = new Map<string, vscode.Uri>();
|
||||
|
||||
function getFileUri(relPath: string): vscode.Uri {
|
||||
let uri = fileUriCache.get(relPath);
|
||||
if (!uri) {
|
||||
uri = vscode.Uri.joinPath(wsRootUri, relPath);
|
||||
fileUriCache.set(relPath, uri);
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
// ---- Lazy line number resolution ----
|
||||
|
||||
/** Cache of file contents for lazy resolution, keyed by absolute path */
|
||||
const fileContentCache = new Map<string, string[] | null>();
|
||||
|
||||
async function getFileLines(absPath: string): Promise<string[] | null> {
|
||||
const cached = fileContentCache.get(absPath);
|
||||
if (cached !== undefined) { return cached; }
|
||||
/**
|
||||
* Get file contents via VS Code's document model.
|
||||
* Uses already-loaded buffers when available, picks up unsaved changes,
|
||||
* and lets VS Code manage its own caching — no duplicate memory usage.
|
||||
*/
|
||||
async function getDocument(uri: vscode.Uri): Promise<vscode.TextDocument | null> {
|
||||
try {
|
||||
const content = await fs.readFile(absPath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
fileContentCache.set(absPath, lines);
|
||||
return lines;
|
||||
return await vscode.workspace.openTextDocument(uri);
|
||||
} catch {
|
||||
fileContentCache.set(absPath, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -247,7 +269,7 @@ async function getFileLines(absPath: string): Promise<string[] | null> {
|
||||
* Resolve the line number of an entry lazily. If already resolved, returns immediately.
|
||||
* Otherwise reads the source file (with caching) and finds the pattern.
|
||||
*/
|
||||
async function resolveLineNumber(entry: CtagsEntry, root: string): Promise<number> {
|
||||
async function resolveLineNumber(entry: CtagsEntry, rootUri: vscode.Uri): Promise<number> {
|
||||
if (entry.lineNumber >= 0) { return entry.lineNumber; }
|
||||
|
||||
const searchText = extractSearchText(entry.pattern);
|
||||
@@ -256,15 +278,14 @@ async function resolveLineNumber(entry: CtagsEntry, root: string): Promise<numbe
|
||||
return 0;
|
||||
}
|
||||
|
||||
const absPath = path.join(root, entry.file);
|
||||
const lines = await getFileLines(absPath);
|
||||
if (!lines) {
|
||||
const doc = await getDocument(getFileUri(entry.file));
|
||||
if (!doc) {
|
||||
entry.lineNumber = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].includes(searchText)) {
|
||||
for (let i = 0; i < doc.lineCount; i++) {
|
||||
if (doc.lineAt(i).text.includes(searchText)) {
|
||||
entry.lineNumber = i;
|
||||
return i;
|
||||
}
|
||||
@@ -275,36 +296,18 @@ async function resolveLineNumber(entry: CtagsEntry, root: string): Promise<numbe
|
||||
|
||||
/**
|
||||
* Resolve line numbers for a batch of entries.
|
||||
* Groups by file to avoid redundant reads.
|
||||
*/
|
||||
async function resolveEntries(entries: CtagsEntry[], root: string): Promise<void> {
|
||||
// Collect unique files that need resolution
|
||||
const filesToResolve = new Set<string>();
|
||||
async function resolveEntries(entries: CtagsEntry[], rootUri: vscode.Uri): Promise<void> {
|
||||
for (const e of entries) {
|
||||
if (e.lineNumber === -1) {
|
||||
filesToResolve.add(e.file);
|
||||
}
|
||||
}
|
||||
// Pre-load files in parallel (up to 20 at a time)
|
||||
const files = Array.from(filesToResolve);
|
||||
const batchSize = 20;
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
const batch = files.slice(i, i + batchSize);
|
||||
await Promise.all(batch.map(f => getFileLines(path.join(root, f))));
|
||||
}
|
||||
// Now resolve all entries (file contents are cached)
|
||||
for (const e of entries) {
|
||||
if (e.lineNumber === -1) {
|
||||
await resolveLineNumber(e, root);
|
||||
await resolveLineNumber(e, rootUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function entryToLocation(entry: CtagsEntry, root: string): vscode.Location {
|
||||
function entryToLocation(entry: CtagsEntry): vscode.Location {
|
||||
const ln = entry.lineNumber >= 0 ? entry.lineNumber : 0;
|
||||
const uri = vscode.Uri.file(path.join(root, entry.file));
|
||||
const pos = new vscode.Position(ln, 0);
|
||||
return new vscode.Location(uri, pos);
|
||||
return new vscode.Location(getFileUri(entry.file), new vscode.Position(ln, 0));
|
||||
}
|
||||
|
||||
function entryToSymbolKind(kind: string): vscode.SymbolKind {
|
||||
@@ -358,6 +361,7 @@ function formatDuration(ms: number): string {
|
||||
|
||||
let db: TagDatabase = { entries: [], nameIndex: new Map(), fileIndex: new Map(), sorted: [] };
|
||||
let wsRoot = "";
|
||||
let wsRootUri: vscode.Uri;
|
||||
let statusBarItem: vscode.StatusBarItem;
|
||||
let isLoading = false;
|
||||
|
||||
@@ -376,7 +380,7 @@ function updateStatusBar() {
|
||||
statusBarItem.show();
|
||||
}
|
||||
|
||||
async function loadTags(): Promise<boolean> {
|
||||
async function loadTags(progress?: vscode.Progress<{ message?: string; increment?: number }>): Promise<boolean> {
|
||||
if (!wsRoot) { return false; }
|
||||
if (isLoading) {
|
||||
logInfo("Load already in progress, skipping.");
|
||||
@@ -391,17 +395,23 @@ async function loadTags(): Promise<boolean> {
|
||||
|
||||
try {
|
||||
// Stage 1: Stream-parse the file
|
||||
logInfo("Stage 1/3: Stream-parsing tags file...");
|
||||
logInfo("Stage 1/2: Stream-parsing tags file...");
|
||||
const t1 = performance.now();
|
||||
|
||||
let fileSizeKB = "?";
|
||||
let fileSizeBytes = 0;
|
||||
try {
|
||||
const stat = await fs.stat(tagsPath);
|
||||
fileSizeKB = (stat.size / 1024).toFixed(1);
|
||||
const stat = await vscode.workspace.fs.stat(vscode.Uri.file(tagsPath));
|
||||
fileSizeBytes = stat.size;
|
||||
fileSizeKB = (fileSizeBytes / 1024).toFixed(1);
|
||||
logInfo(` File size: ${fileSizeKB} KB`);
|
||||
} catch { /* stat failed, continue anyway */ }
|
||||
|
||||
const entries = await streamParseCtagsFile(tagsPath);
|
||||
const entries = await streamParseCtagsFile(tagsPath, (count) => {
|
||||
const msg = `Parsing tags... ${(count / 1000).toFixed(0)}K entries`;
|
||||
statusBarItem.text = `$(sync~spin) vsctags: ${(count / 1000).toFixed(0)}K`;
|
||||
if (progress) { progress.report({ message: msg }); }
|
||||
});
|
||||
const parseTime = performance.now() - t1;
|
||||
const withLineNum = entries.filter(e => e.lineNumber >= 0).length;
|
||||
const needsResolve = entries.length - withLineNum;
|
||||
@@ -409,7 +419,9 @@ async function loadTags(): Promise<boolean> {
|
||||
logInfo(` ${withLineNum} with line numbers, ${needsResolve} need lazy resolution`);
|
||||
|
||||
// Stage 2: Build indexes (name, file, sorted)
|
||||
logInfo("Stage 2/3: Building indexes...");
|
||||
logInfo("Stage 2/2: Building indexes...");
|
||||
if (progress) { progress.report({ message: `Building index for ${entries.length} tags...` }); }
|
||||
statusBarItem.text = `$(sync~spin) vsctags: indexing...`;
|
||||
const t2 = performance.now();
|
||||
const newDb = buildDatabase(entries);
|
||||
const indexTime = performance.now() - t2;
|
||||
@@ -418,11 +430,8 @@ async function loadTags(): Promise<boolean> {
|
||||
logInfo(` Sorted array: ${newDb.sorted.length} entries`);
|
||||
logInfo(` Index build took ${formatDuration(indexTime)}`);
|
||||
|
||||
// Stage 3: Clear file content cache (stale data from previous load)
|
||||
logInfo("Stage 3/3: Clearing resolution cache...");
|
||||
fileContentCache.clear();
|
||||
|
||||
// Commit
|
||||
// Commit — clear caches before swapping
|
||||
fileUriCache.clear();
|
||||
db = newDb;
|
||||
|
||||
// Summary
|
||||
@@ -475,9 +484,9 @@ class CtagsDefinitionProvider implements vscode.DefinitionProvider {
|
||||
const entries = db.nameIndex.get(word);
|
||||
if (!entries || entries.length === 0) { return undefined; }
|
||||
// Lazy-resolve line numbers before navigating
|
||||
await resolveEntries(entries, wsRoot);
|
||||
await resolveEntries(entries, wsRootUri);
|
||||
logInfo(`Definition lookup: "${word}" -> ${entries.length} result(s)`);
|
||||
return entries.map((e) => entryToLocation(e, wsRoot));
|
||||
return entries.map((e) => entryToLocation(e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,7 +499,7 @@ class CtagsHoverProvider implements vscode.HoverProvider {
|
||||
if (!word) { return undefined; }
|
||||
const entries = db.nameIndex.get(word);
|
||||
if (!entries || entries.length === 0) { return undefined; }
|
||||
await resolveEntries(entries, wsRoot);
|
||||
await resolveEntries(entries, wsRootUri);
|
||||
|
||||
const lines: string[] = [];
|
||||
for (const entry of entries) {
|
||||
@@ -505,44 +514,72 @@ class CtagsHoverProvider implements vscode.HoverProvider {
|
||||
}
|
||||
|
||||
class CtagsWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
|
||||
provideWorkspaceSymbols(query: string): vscode.SymbolInformation[] {
|
||||
public async provideWorkspaceSymbols(query: string, token: vscode.CancellationToken): Promise<vscode.SymbolInformation[]> {
|
||||
const t = performance.now();
|
||||
const limit = 500;
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const limit = 200;
|
||||
const lowerQuery = query.toLowerCase().trim();
|
||||
|
||||
let results: CtagsEntry[];
|
||||
|
||||
if (lowerQuery.length === 0) {
|
||||
// No query: return first N entries
|
||||
results = db.entries.slice(0, limit);
|
||||
} else {
|
||||
// Try prefix search first (O(log n + k))
|
||||
results = prefixSearch(db, lowerQuery, limit);
|
||||
|
||||
// If prefix didn't fill up, supplement with substring matches
|
||||
if (results.length < limit) {
|
||||
const prefixSet = new Set(results);
|
||||
const substringResults = substringSearch(db, lowerQuery, limit);
|
||||
for (const entry of substringResults) {
|
||||
if (!prefixSet.has(entry)) {
|
||||
results.push(entry);
|
||||
if (results.length >= limit) { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!lowerQuery) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const symbols = results.map(entry =>
|
||||
new vscode.SymbolInformation(
|
||||
entry.name,
|
||||
entryToSymbolKind(entry.kind),
|
||||
entry.scope,
|
||||
entryToLocation(entry, wsRoot),
|
||||
),
|
||||
);
|
||||
if (token.isCancellationRequested) {
|
||||
throw new vscode.CancellationError();
|
||||
}
|
||||
|
||||
logInfo(`Workspace symbol search: "${query}" -> ${symbols.length} result(s) in ${formatDuration(performance.now() - t)}`);
|
||||
return symbols;
|
||||
try {
|
||||
// Fast prefix search via binary search — O(log n + k), synchronous
|
||||
const results = prefixSearch(db, lowerQuery, limit);
|
||||
|
||||
if (token.isCancellationRequested) {``
|
||||
throw new vscode.CancellationError();
|
||||
}
|
||||
|
||||
const symbols = results.map(entry =>
|
||||
new vscode.SymbolInformation(
|
||||
entry.name,
|
||||
entryToSymbolKind(entry.kind),
|
||||
entry.scope,
|
||||
entryToLocation(entry),
|
||||
),
|
||||
);
|
||||
|
||||
if (symbols.length > 0) {
|
||||
const s = symbols[0];
|
||||
logInfo(`Workspace symbol search: "${query}" -> ${symbols.length} result(s) in ${formatDuration(performance.now() - t)}, sample: uri=${s.location.uri.toString()}, scheme=${s.location.uri.scheme}, line=${s.location.range.start.line}`);
|
||||
} else {
|
||||
logInfo(`Workspace symbol search: "${query}" -> 0 result(s) in ${formatDuration(performance.now() - t)}`);
|
||||
}
|
||||
return symbols;
|
||||
} catch (err) {
|
||||
if (err instanceof vscode.CancellationError) {
|
||||
logInfo(`Workspace symbol search cancelled for "${query}" after ${formatDuration(performance.now() - t)}`);
|
||||
throw err;
|
||||
}
|
||||
logError(`Workspace symbol search failed for "${query}": ${err}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async resolveWorkspaceSymbol(symbol: vscode.SymbolInformation): Promise<vscode.SymbolInformation | undefined> {
|
||||
const t = performance.now();
|
||||
// Resolve the exact line number when the user selects a symbol
|
||||
const relPath = vscode.workspace.asRelativePath(symbol.location.uri, false);
|
||||
const entries = db.nameIndex.get(symbol.name);
|
||||
if (!entries) {
|
||||
logInfo(`resolveWorkspaceSymbol: "${symbol.name}" no entries, ${formatDuration(performance.now() - t)}`);
|
||||
return symbol;
|
||||
}
|
||||
|
||||
const match = entries.find(e => e.file === relPath);
|
||||
if (match) {
|
||||
if (match.lineNumber === -1) {
|
||||
await resolveLineNumber(match, wsRootUri);
|
||||
}
|
||||
symbol.location = entryToLocation(match);
|
||||
}
|
||||
logInfo(`resolveWorkspaceSymbol: "${symbol.name}" in ${formatDuration(performance.now() - t)}`);
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,7 +602,7 @@ class CtagsDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
|
||||
entry.name,
|
||||
entryToSymbolKind(entry.kind),
|
||||
entry.scope,
|
||||
entryToLocation(entry, wsRoot),
|
||||
entryToLocation(entry),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -583,9 +620,9 @@ class CtagsReferenceProvider implements vscode.ReferenceProvider {
|
||||
if (!word) { return undefined; }
|
||||
const entries = db.nameIndex.get(word);
|
||||
if (!entries || entries.length === 0) { return undefined; }
|
||||
await resolveEntries(entries, wsRoot);
|
||||
await resolveEntries(entries, wsRootUri);
|
||||
logInfo(`References lookup: "${word}" -> ${entries.length} result(s)`);
|
||||
return entries.map((e) => entryToLocation(e, wsRoot));
|
||||
return entries.map((e) => entryToLocation(e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,7 +639,8 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
logInfo("No workspace folder open. Extension idle.");
|
||||
return;
|
||||
}
|
||||
wsRoot = folders[0].uri.fsPath;
|
||||
wsRootUri = folders[0].uri;
|
||||
wsRoot = wsRootUri.fsPath;
|
||||
logInfo(`Workspace root: ${wsRoot}`);
|
||||
|
||||
// Status bar
|
||||
@@ -612,10 +650,10 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
// Initial load with progress
|
||||
vscode.window.withProgress(
|
||||
{ location: vscode.ProgressLocation.Window, title: "vsctags: Loading tags..." },
|
||||
{ location: vscode.ProgressLocation.Window, title: "vsctags" },
|
||||
async (progress) => {
|
||||
progress.report({ message: "Reading tags file..." });
|
||||
const ok = await loadTags();
|
||||
progress.report({ message: "Loading tags..." });
|
||||
const ok = await loadTags(progress);
|
||||
if (ok) {
|
||||
progress.report({ message: `Loaded ${db.entries.length} tags` });
|
||||
}
|
||||
@@ -634,24 +672,31 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
);
|
||||
logInfo("Language providers registered (definition, hover, workspace symbols, document symbols, references)");
|
||||
|
||||
// Watch the tags file for changes
|
||||
// Watch the tags file for changes (debounced)
|
||||
const tagsPattern = new vscode.RelativePattern(folders[0], "tags");
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(tagsPattern);
|
||||
const debounceMs = 2000;
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
watcher.onDidChange(async () => {
|
||||
logInfo("Tags file changed on disk. Reloading...");
|
||||
await loadTags();
|
||||
vscode.window.showInformationMessage(`[vsctags] Reloaded ${db.entries.length} tags.`);
|
||||
});
|
||||
watcher.onDidCreate(async () => {
|
||||
logInfo("Tags file created. Loading...");
|
||||
await loadTags();
|
||||
vscode.window.showInformationMessage(`[vsctags] Loaded ${db.entries.length} tags.`);
|
||||
});
|
||||
function debouncedReload(reason: string) {
|
||||
if (debounceTimer) { clearTimeout(debounceTimer); }
|
||||
logInfo(`Tags file ${reason}. Waiting ${debounceMs}ms for stability...`);
|
||||
statusBarItem.text = "$(sync~spin) vsctags: file changed...";
|
||||
debounceTimer = setTimeout(async () => {
|
||||
debounceTimer = undefined;
|
||||
logInfo("Debounce elapsed, reloading tags.");
|
||||
await loadTags();
|
||||
vscode.window.showInformationMessage(`[vsctags] Reloaded ${db.entries.length} tags.`);
|
||||
}, debounceMs);
|
||||
}
|
||||
|
||||
watcher.onDidChange(() => debouncedReload("changed"));
|
||||
watcher.onDidCreate(() => debouncedReload("created"));
|
||||
watcher.onDidDelete(() => {
|
||||
if (debounceTimer) { clearTimeout(debounceTimer); debounceTimer = undefined; }
|
||||
logInfo("Tags file deleted.");
|
||||
db = { entries: [], nameIndex: new Map(), fileIndex: new Map(), sorted: [] };
|
||||
fileContentCache.clear();
|
||||
fileUriCache.clear();
|
||||
updateStatusBar();
|
||||
vscode.window.showInformationMessage("[vsctags] Tags file removed.");
|
||||
});
|
||||
@@ -665,7 +710,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
logInfo("Manual reload requested.");
|
||||
const ok = await vscode.window.withProgress(
|
||||
{ location: vscode.ProgressLocation.Notification, title: "vsctags: Reloading tags..." },
|
||||
async () => loadTags(),
|
||||
async (progress) => loadTags(progress),
|
||||
);
|
||||
if (ok) {
|
||||
vscode.window.showInformationMessage(`[vsctags] Reloaded ${db.entries.length} tags.`);
|
||||
@@ -682,10 +727,217 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
}),
|
||||
);
|
||||
|
||||
// Welcome page command
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("vsctags.showWelcome", () => {
|
||||
showWelcomePage(context);
|
||||
}),
|
||||
);
|
||||
|
||||
// Auto-show welcome page on first install
|
||||
const hasShownWelcome = context.globalState.get<boolean>("vsctags.welcomeShown");
|
||||
if (!hasShownWelcome) {
|
||||
showWelcomePage(context);
|
||||
context.globalState.update("vsctags.welcomeShown", true);
|
||||
}
|
||||
|
||||
logInfo("Extension activated.");
|
||||
}
|
||||
|
||||
// ---- Welcome Page ----
|
||||
|
||||
async function showWelcomePage(context: vscode.ExtensionContext) {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
"vsctags.welcome",
|
||||
"vsctags — Welcome",
|
||||
vscode.ViewColumn.One,
|
||||
{ enableScripts: false },
|
||||
);
|
||||
|
||||
let markdown = "";
|
||||
try {
|
||||
// Try both cases — vsce may lowercase the filename in the VSIX
|
||||
let bytes: Uint8Array | undefined;
|
||||
for (const name of ["README.md", "readme.md"]) {
|
||||
try {
|
||||
bytes = await vscode.workspace.fs.readFile(vscode.Uri.joinPath(context.extensionUri, name));
|
||||
break;
|
||||
} catch { /* try next */ }
|
||||
}
|
||||
if (bytes) {
|
||||
markdown = Buffer.from(bytes).toString("utf-8");
|
||||
} else {
|
||||
logError(`README not found at extensionUri: ${context.extensionUri.toString()}`);
|
||||
markdown = "# vsctags\n\nREADME.md not found.";
|
||||
}
|
||||
} catch {
|
||||
markdown = "# vsctags\n\nREADME.md not found.";
|
||||
}
|
||||
|
||||
panel.webview.html = renderMarkdownHtml(markdown);
|
||||
}
|
||||
|
||||
/** Minimal Markdown to HTML renderer — no dependencies */
|
||||
function renderMarkdownHtml(md: string): string {
|
||||
let html = escapeHtml(md);
|
||||
|
||||
// Code blocks (``` ... ```)
|
||||
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, lang, code) => {
|
||||
return `<pre><code class="language-${lang}">${code.trim()}</code></pre>`;
|
||||
});
|
||||
|
||||
// Inline code
|
||||
html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
|
||||
|
||||
// Tables
|
||||
html = html.replace(/((?:^\|.+\|\s*$\n?)+)/gm, (_m, table: string) => {
|
||||
const rows = table.trim().split("\n").filter(r => r.trim().length > 0);
|
||||
if (rows.length < 2) { return table; }
|
||||
|
||||
// Check if second row is separator (|---|---|)
|
||||
const isSep = /^\|[\s\-:|]+\|$/.test(rows[1].trim());
|
||||
if (!isSep) { return table; }
|
||||
|
||||
const parseRow = (row: string) =>
|
||||
row.split("|").slice(1, -1).map(c => c.trim());
|
||||
|
||||
const headerCells = parseRow(rows[0]);
|
||||
const thead = "<tr>" + headerCells.map(c => `<th>${c}</th>`).join("") + "</tr>";
|
||||
|
||||
const bodyRows = rows.slice(2).map(row => {
|
||||
const cells = parseRow(row);
|
||||
return "<tr>" + cells.map(c => `<td>${c}</td>`).join("") + "</tr>";
|
||||
}).join("\n");
|
||||
|
||||
return `<table><thead>${thead}</thead><tbody>${bodyRows}</tbody></table>`;
|
||||
});
|
||||
|
||||
// Headers
|
||||
html = html.replace(/^######\s+(.+)$/gm, "<h6>$1</h6>");
|
||||
html = html.replace(/^#####\s+(.+)$/gm, "<h5>$1</h5>");
|
||||
html = html.replace(/^####\s+(.+)$/gm, "<h4>$1</h4>");
|
||||
html = html.replace(/^###\s+(.+)$/gm, "<h3>$1</h3>");
|
||||
html = html.replace(/^##\s+(.+)$/gm, "<h2>$1</h2>");
|
||||
html = html.replace(/^#\s+(.+)$/gm, "<h1>$1</h1>");
|
||||
|
||||
// Bold + italic
|
||||
html = html.replace(/\*\*\*(.+?)\*\*\*/g, "<strong><em>$1</em></strong>");
|
||||
html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
||||
html = html.replace(/\*(.+?)\*/g, "<em>$1</em>");
|
||||
|
||||
// Links
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
||||
|
||||
// Unordered lists
|
||||
html = html.replace(/((?:^- .+$\n?)+)/gm, (_m, block: string) => {
|
||||
const items = block.trim().split("\n").map(l => `<li>${l.replace(/^- /, "")}</li>`);
|
||||
return `<ul>${items.join("\n")}</ul>`;
|
||||
});
|
||||
|
||||
// Ordered lists
|
||||
html = html.replace(/((?:^\d+\.\s.+$\n?)+)/gm, (_m, block: string) => {
|
||||
const items = block.trim().split("\n").map(l => `<li>${l.replace(/^\d+\.\s/, "")}</li>`);
|
||||
return `<ol>${items.join("\n")}</ol>`;
|
||||
});
|
||||
|
||||
// Horizontal rules
|
||||
html = html.replace(/^---$/gm, "<hr>");
|
||||
|
||||
// Paragraphs — wrap remaining loose lines
|
||||
html = html.replace(/^(?!<[a-z])(\S.+)$/gm, "<p>$1</p>");
|
||||
|
||||
// Clean up empty lines
|
||||
html = html.replace(/\n{3,}/g, "\n\n");
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
font-family: var(--vscode-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
||||
font-size: var(--vscode-font-size, 14px);
|
||||
color: var(--vscode-foreground);
|
||||
background: var(--vscode-editor-background);
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--vscode-foreground);
|
||||
border-bottom: 1px solid var(--vscode-panel-border, #333);
|
||||
padding-bottom: 4px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
h1 { font-size: 2em; }
|
||||
h2 { font-size: 1.5em; }
|
||||
h3 { font-size: 1.25em; }
|
||||
code {
|
||||
font-family: var(--vscode-editor-font-family, 'Menlo', 'Consolas', monospace);
|
||||
background: var(--vscode-textCodeBlock-background, rgba(128,128,128,0.15));
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
pre {
|
||||
background: var(--vscode-textCodeBlock-background, rgba(128,128,128,0.15));
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid var(--vscode-panel-border, #444);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: var(--vscode-textCodeBlock-background, rgba(128,128,128,0.15));
|
||||
font-weight: 600;
|
||||
}
|
||||
a {
|
||||
color: var(--vscode-textLink-foreground, #3794ff);
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
ul, ol {
|
||||
padding-left: 24px;
|
||||
}
|
||||
li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--vscode-panel-border, #333);
|
||||
margin: 24px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${html}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
logInfo("Extension deactivated.");
|
||||
fileContentCache.clear();
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
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));
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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`.
|
||||
|
||||
## 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
|
||||
|
||||
* [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns.
|
||||
* 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).
|
||||
* Integrate to the [report issue](https://code.visualstudio.com/api/get-started/wrapping-up#issue-reporting) flow to get issue and feature requests reported by users.
|
||||
Reference in New Issue
Block a user