diff --git a/README.md b/README.md
index ecd7f0f..3ec5c3b 100644
--- a/README.md
+++ b/README.md
@@ -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.
@@ -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
@@ -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
diff --git a/assets/implementation-dark.svg b/assets/implementation-dark.svg
new file mode 100644
index 0000000..2c27a78
--- /dev/null
+++ b/assets/implementation-dark.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/implementation-light.svg b/assets/implementation-light.svg
new file mode 100644
index 0000000..c7ff0e6
--- /dev/null
+++ b/assets/implementation-light.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/override-dark.svg b/assets/override-dark.svg
new file mode 100644
index 0000000..1451998
--- /dev/null
+++ b/assets/override-dark.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/override-light.svg b/assets/override-light.svg
new file mode 100644
index 0000000..6b12ecc
--- /dev/null
+++ b/assets/override-light.svg
@@ -0,0 +1,6 @@
+
diff --git a/package.json b/package.json
index 4b6e161..331daa6 100644
--- a/package.json
+++ b/package.json
@@ -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."
}
}
}
diff --git a/src/extension.ts b/src/extension.ts
index 84fc369..cb88bc5 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -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;
@@ -25,25 +32,49 @@ export function activate(context: vscode.ExtensionContext) {
// Debounce
const delay = vscode.workspace.getConfiguration('pythonOverrideMark').get('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);
};
@@ -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();
}
@@ -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('gutterIcons.enabled', false);
+
+ if (!gutterIconsEnabled) {
+ gutterManager.clear();
+ }
+ }
+
+ if (activeEditor?.document.languageId === 'python') {
+ triggerUpdate();
+ } else {
+ gutterManager.clear();
+ hoverProvider.updateResults(undefined, []);
+ }
})
);
diff --git a/src/gutterManager.ts b/src/gutterManager.ts
new file mode 100644
index 0000000..df42fbb
--- /dev/null
+++ b/src/gutterManager.ts
@@ -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('gutterIcons.enabled', false);
+ }
+
+ private clearEditor(editor: vscode.TextEditor): void {
+ editor.setDecorations(this.overrideDecorationType, []);
+ editor.setDecorations(this.implementationDecorationType, []);
+ }
+}
diff --git a/src/hoverProvider.ts b/src/hoverProvider.ts
new file mode 100644
index 0000000..523e300
--- /dev/null
+++ b/src/hoverProvider.ts
@@ -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('gutterIcons.enabled', false);
+ }
+}