Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Inline CodeLens indicators for overridden and implemented methods in Python.

- **Cross-File Support**: Works across your workspace.
- **Implementation Detection**: Identifies methods that are implemented/overridden by parent/subclasses.
- **Optional Gutter Markers**: Shows gutter icons for overrides and subclass implementations with hover details.
- **CodeLens Navigation**:
- **Overrides**: Click the "Overrides Parent.method" CodeLens to navigate to the parent definition.
- **Implementations**: Click "Implemented in Child.method" to navigate to the subclass implementation.
Expand All @@ -45,6 +46,7 @@ Inline CodeLens indicators for overridden and implemented methods in Python.

| Setting | Description |
|------------------------------------------ |---------------------------------------------------------------------------------------------------------- |
| `pythonOverrideMark.gutterIcons.enabled` | Enable gutter icons for overrides and methods implemented in subclasses. Default is `false` |
| `pythonOverrideMark.maxInheritanceDepth` | Maximum depth to search for parent classes (recursive inheritance). Default is 3. Set to 0 for unlimited |

## How it Works
Expand All @@ -53,7 +55,8 @@ This extension uses the VS Code Python extension's Language Server Protocol (LSP

1. **Override Detection**: It scans the active document for class definitions and resolves their parent classes (even across files). It then compares methods to identify overrides.
2. **Implementation Detection**: It finds references to the current class to identify subclasses. It then checks those subclasses for methods that implement or override methods in the parent.
3. **Performance**: Detection is debounced (default 500ms) and optimized to skip non-class symbols to ensure a smooth editing experience.
3. **Gutter Markers**: Gutter icons are visual indicators with hover details. Navigation still goes through the existing CodeLens commands.
4. **Performance**: Detection is debounced (default 500ms) and optimized to skip non-class symbols to ensure a smooth editing experience.

## Future Improvements

Expand Down
6 changes: 6 additions & 0 deletions assets/implementation-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions assets/implementation-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions assets/override-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions assets/override-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
"type": "number",
"default": 3,
"description": "Maximum depth to search for parent classes (recursive inheritance). Set to 0 for unlimited (use with caution)."
},
"pythonOverrideMark.gutterIcons.enabled": {
"type": "boolean",
"default": false,
"description": "Show gutter icons for overridden methods and methods implemented in subclasses. Disabled by default."
}
}
}
Expand Down
84 changes: 69 additions & 15 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@ import * as vscode from 'vscode';
import { OverrideDetector } from './overrideDetector';
import { OverrideCodeLensProvider } from './codeLensProvider';
import { SubclassCache, ReferenceClassificationCache } from './caching';
import { OverrideGutterManager } from './gutterManager';
import { OverrideHoverProvider } from './hoverProvider';

export function activate(context: vscode.ExtensionContext) {


const detector = new OverrideDetector();
const codeLensProvider = new OverrideCodeLensProvider();
const gutterManager = new OverrideGutterManager(context.extensionUri);
const hoverProvider = new OverrideHoverProvider();

context.subscriptions.push(gutterManager);

// Register CodeLens Provider
context.subscriptions.push(
vscode.languages.registerCodeLensProvider({ language: 'python', scheme: 'file' }, codeLensProvider)
vscode.languages.registerCodeLensProvider({ language: 'python', scheme: 'file' }, codeLensProvider),
vscode.languages.registerHoverProvider({ language: 'python', scheme: 'file' }, hoverProvider)
);

let activeEditor = vscode.window.activeTextEditor;
Expand All @@ -25,25 +32,49 @@ export function activate(context: vscode.ExtensionContext) {
// Debounce
const delay = vscode.workspace.getConfiguration('pythonOverrideMark').get<number>('debounceDelay', 500);
timeout = setTimeout(() => {
if (activeEditor && activeEditor.document.languageId === 'python') {
// Ensure Python extension is activated
const pythonExtension = vscode.extensions.getExtension('ms-python.python');
if (pythonExtension && !pythonExtension.isActive) {
const editor = activeEditor;

pythonExtension.activate().then(() => {
if (!editor) {
gutterManager.clear();
hoverProvider.updateResults(undefined, []);
return;
}

// Re-trigger update after activation
triggerUpdate();
});
return; // Exit to avoid running detection before Python extension is ready
}
if (editor.document.languageId !== 'python') {
gutterManager.clear();
hoverProvider.updateResults(undefined, []);
return;
}

// Ensure Python extension is activated
const pythonExtension = vscode.extensions.getExtension('ms-python.python');
if (pythonExtension && !pythonExtension.isActive) {

detector.detectOverrides(activeEditor).then(items => {
codeLensProvider.updateResults(items);
}).catch(error => {
console.error('Error updating override marks:', error);
pythonExtension.activate().then(() => {

// Re-trigger update after activation
triggerUpdate();
});
return; // Exit to avoid running detection before Python extension is ready
}

detector.detectOverrides(editor).then(items => {
if (editor !== activeEditor) {
return;
}

codeLensProvider.updateResults(items);
gutterManager.update(editor, items);
hoverProvider.updateResults(editor, items);
}).catch(error => {
if (editor === activeEditor) {
codeLensProvider.updateResults([]);
gutterManager.update(editor, []);
hoverProvider.updateResults(editor, []);
}

console.error('Error updating override marks:', error);
});
}, delay);
};

Expand All @@ -55,6 +86,9 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(editor => {
activeEditor = editor;
gutterManager.clear();
hoverProvider.updateResults(undefined, []);

if (editor) {
triggerUpdate();
}
Expand All @@ -76,6 +110,26 @@ export function activate(context: vscode.ExtensionContext) {
if (involvesClass) {
SubclassCache.getInstance().clear();
}
}),
vscode.workspace.onDidChangeConfiguration(event => {
if (!event.affectsConfiguration('pythonOverrideMark')) {
return;
}

if (event.affectsConfiguration('pythonOverrideMark.gutterIcons.enabled')) {
const gutterIconsEnabled = vscode.workspace.getConfiguration('pythonOverrideMark').get<boolean>('gutterIcons.enabled', false);

if (!gutterIconsEnabled) {
gutterManager.clear();
}
}

if (activeEditor?.document.languageId === 'python') {
triggerUpdate();
} else {
gutterManager.clear();
hoverProvider.updateResults(undefined, []);
}
})
);

Expand Down
93 changes: 93 additions & 0 deletions src/gutterManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as vscode from 'vscode';
import { OverrideItem } from './types';

export class OverrideGutterManager implements vscode.Disposable {
private readonly overrideDecorationType: vscode.TextEditorDecorationType;
private readonly implementationDecorationType: vscode.TextEditorDecorationType;
private activeEditor: vscode.TextEditor | undefined;

constructor(extensionUri: vscode.Uri) {
const assetsUri = vscode.Uri.joinPath(extensionUri, 'assets');

this.overrideDecorationType = vscode.window.createTextEditorDecorationType({
isWholeLine: true,
gutterIconSize: 'contain',
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
light: {
gutterIconPath: vscode.Uri.joinPath(assetsUri, 'override-light.svg')
},
dark: {
gutterIconPath: vscode.Uri.joinPath(assetsUri, 'override-dark.svg')
}
});

this.implementationDecorationType = vscode.window.createTextEditorDecorationType({
isWholeLine: true,
gutterIconSize: 'contain',
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
light: {
gutterIconPath: vscode.Uri.joinPath(assetsUri, 'implementation-light.svg')
},
dark: {
gutterIconPath: vscode.Uri.joinPath(assetsUri, 'implementation-dark.svg')
}
});
}

public update(editor: vscode.TextEditor | undefined, items: OverrideItem[]): void {
if (this.activeEditor && this.activeEditor !== editor) {
this.clearEditor(this.activeEditor);
}

this.activeEditor = editor;

if (!editor || editor.document.languageId !== 'python' || !this.isEnabled()) {
if (editor) {
this.clearEditor(editor);
}
return;
}

const overrideDecorations: vscode.DecorationOptions[] = [];
const implementationDecorations: vscode.DecorationOptions[] = [];

for (const item of items) {
if (item.type === 'override' && item.parentMethodName) {
overrideDecorations.push({
range: item.range
});
}

if (item.type === 'implementation' && item.childMethods && item.childMethods.length > 0) {
implementationDecorations.push({
range: item.range
});
}
}

editor.setDecorations(this.overrideDecorationType, overrideDecorations);
editor.setDecorations(this.implementationDecorationType, implementationDecorations);
}

public clear(): void {
if (this.activeEditor) {
this.clearEditor(this.activeEditor);
this.activeEditor = undefined;
}
}

public dispose(): void {
this.clear();
this.overrideDecorationType.dispose();
this.implementationDecorationType.dispose();
}

private isEnabled(): boolean {
return vscode.workspace.getConfiguration('pythonOverrideMark').get<boolean>('gutterIcons.enabled', false);
}

private clearEditor(editor: vscode.TextEditor): void {
editor.setDecorations(this.overrideDecorationType, []);
editor.setDecorations(this.implementationDecorationType, []);
}
}
69 changes: 69 additions & 0 deletions src/hoverProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as vscode from 'vscode';
import { OverrideItem } from './types';

export class OverrideHoverProvider implements vscode.HoverProvider {
private documentUri: string | undefined;
private items: OverrideItem[] = [];

public updateResults(editor: vscode.TextEditor | undefined, items: OverrideItem[]): void {
this.documentUri = editor?.document.uri.toString();
this.items = items;
}

public provideHover(document: vscode.TextDocument, position: vscode.Position): vscode.Hover | undefined {
if (!this.isEnabled()) {
return undefined;
}

if (document.uri.toString() !== this.documentUri) {
return undefined;
}

const messages: string[] = [];
let hoverRange: vscode.Range | undefined;

for (const item of this.items) {
const lineRange = document.lineAt(item.range.start.line).range;
if (!lineRange.contains(position)) {
continue;
}

const message = this.getHoverMessage(item);
if (!message) {
continue;
}

if (!messages.includes(message)) {
messages.push(message);
}

hoverRange = hoverRange
? hoverRange.union(lineRange)
: lineRange;
}

if (messages.length === 0 || !hoverRange) {
return undefined;
}

return new vscode.Hover(messages.join('\n\n'), hoverRange);
}

private getHoverMessage(item: OverrideItem): string | undefined {
if (item.type === 'override' && item.parentMethodName) {
return `Overrides ${item.parentMethodName}`;
}

if (item.type === 'implementation' && item.childMethods && item.childMethods.length > 0) {
return item.childMethods.length === 1
? 'Implemented in 1 subclass'
: `Implemented in ${item.childMethods.length} subclasses`;
}

return undefined;
}

private isEnabled(): boolean {
return vscode.workspace.getConfiguration('pythonOverrideMark').get<boolean>('gutterIcons.enabled', false);
}
}
Loading