139 lines
4.1 KiB
TypeScript
139 lines
4.1 KiB
TypeScript
import * as vscode from "vscode";
|
|
import type { SfClient } from "./sf-client.js";
|
|
|
|
/**
|
|
* Patterns that identify the start of a named function, class, or method
|
|
* declaration in common languages. Each entry captures the symbol name in
|
|
* capture group 1.
|
|
*/
|
|
const SYMBOL_PATTERNS: { languages: string[]; regex: RegExp }[] = [
|
|
{
|
|
// TypeScript / JavaScript: function foo(...) | async function foo(...)
|
|
languages: ["typescript", "typescriptreact", "javascript", "javascriptreact"],
|
|
regex: /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*[(<]/,
|
|
},
|
|
{
|
|
// TypeScript / JavaScript: class Foo
|
|
languages: ["typescript", "typescriptreact", "javascript", "javascriptreact"],
|
|
regex: /^\s*(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/,
|
|
},
|
|
{
|
|
// TypeScript / JavaScript: method declarations inside a class
|
|
// foo(...) { | async foo(...) { | private foo(...): T {
|
|
languages: ["typescript", "typescriptreact", "javascript", "javascriptreact"],
|
|
regex: /^\s*(?:(?:public|private|protected|static|async|readonly)\s+)*(\w+)\s*\(/,
|
|
},
|
|
{
|
|
// Python: def foo( | async def foo(
|
|
languages: ["python"],
|
|
regex: /^\s*(?:async\s+)?def\s+(\w+)\s*\(/,
|
|
},
|
|
{
|
|
// Python: class Foo
|
|
languages: ["python"],
|
|
regex: /^\s*class\s+(\w+)/,
|
|
},
|
|
{
|
|
// Go: func foo( | func (r Receiver) foo(
|
|
languages: ["go"],
|
|
regex: /^\s*func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(/,
|
|
},
|
|
{
|
|
// Rust: fn foo( | pub fn foo( | async fn foo(
|
|
languages: ["rust"],
|
|
regex: /^\s*(?:pub(?:\([^)]+\))?\s+)?(?:async\s+)?fn\s+(\w+)\s*[(<]/,
|
|
},
|
|
];
|
|
|
|
/**
|
|
* CodeLensProvider that adds an "Ask SF" lens above named function and class
|
|
* declarations. Clicking the lens sends a brief explanation request to the SF
|
|
* agent for that specific symbol.
|
|
*/
|
|
export class SfCodeLensProvider implements vscode.CodeLensProvider, vscode.Disposable {
|
|
private readonly _onDidChangeCodeLenses = new vscode.EventEmitter<void>();
|
|
readonly onDidChangeCodeLenses = this._onDidChangeCodeLenses.event;
|
|
|
|
private disposables: vscode.Disposable[] = [];
|
|
|
|
constructor(private readonly client: SfClient) {
|
|
this.disposables.push(
|
|
this._onDidChangeCodeLenses,
|
|
client.onConnectionChange(() => this._onDidChangeCodeLenses.fire()),
|
|
vscode.workspace.onDidChangeConfiguration((e) => {
|
|
if (e.affectsConfiguration("sf.codeLens")) {
|
|
this._onDidChangeCodeLenses.fire();
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
provideCodeLenses(
|
|
document: vscode.TextDocument,
|
|
_token: vscode.CancellationToken,
|
|
): vscode.CodeLens[] {
|
|
const lenses: vscode.CodeLens[] = [];
|
|
|
|
if (!vscode.workspace.getConfiguration("sf").get<boolean>("codeLens", true)) {
|
|
return lenses;
|
|
}
|
|
const langId = document.languageId;
|
|
const patterns = SYMBOL_PATTERNS.filter((p) => p.languages.includes(langId));
|
|
|
|
if (patterns.length === 0) {
|
|
return lenses;
|
|
}
|
|
|
|
const fileName = document.fileName.split(/[\\/]/).pop() ?? document.fileName;
|
|
const seen = new Set<number>();
|
|
|
|
for (let i = 0; i < document.lineCount; i++) {
|
|
const text = document.lineAt(i).text;
|
|
|
|
for (const { regex } of patterns) {
|
|
const match = regex.exec(text);
|
|
if (match && match[1] && !seen.has(i)) {
|
|
seen.add(i);
|
|
const symbolName = match[1];
|
|
const range = new vscode.Range(i, 0, i, text.length);
|
|
const args = [symbolName, fileName, i + 1];
|
|
|
|
lenses.push(
|
|
new vscode.CodeLens(range, {
|
|
title: "$(hubot) Ask SF",
|
|
tooltip: `Ask SF to explain ${symbolName}`,
|
|
command: "sf.askAboutSymbol",
|
|
arguments: args,
|
|
}),
|
|
new vscode.CodeLens(range, {
|
|
title: "$(pencil) Refactor",
|
|
tooltip: `Refactor ${symbolName}`,
|
|
command: "sf.refactorSymbol",
|
|
arguments: args,
|
|
}),
|
|
new vscode.CodeLens(range, {
|
|
title: "$(bug) Find Bugs",
|
|
tooltip: `Review ${symbolName} for bugs`,
|
|
command: "sf.findBugsSymbol",
|
|
arguments: args,
|
|
}),
|
|
new vscode.CodeLens(range, {
|
|
title: "$(beaker) Tests",
|
|
tooltip: `Generate tests for ${symbolName}`,
|
|
command: "sf.generateTestsSymbol",
|
|
arguments: args,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return lenses;
|
|
}
|
|
|
|
dispose(): void {
|
|
for (const d of this.disposables) {
|
|
d.dispose();
|
|
}
|
|
}
|
|
}
|