From 7970af945730beae68daba7b859e66da30f91884 Mon Sep 17 00:00:00 2001 From: showpiecep Date: Sun, 19 Apr 2026 17:40:45 +0300 Subject: [PATCH 1/3] feat: add optional gutter markers for Python overrides and implementations Introduce a dedicated gutter decoration layer that reuses the existing override/implementation analysis without changing the detector logic. Changes included: - add OverrideGutterManager to render gutter icons for: - methods overriding a parent method - methods implemented in subclasses - wire gutter updates into the extension lifecycle alongside the existing CodeLens flow - keep CodeLens as the primary navigation mechanism - add a HoverProvider so users get a single, non-duplicated explanation on the method line - add light/dark SVG assets for override and implementation markers - add the pythonOverrideMark.gutterIcons.enabled setting - update README with the new optional gutter marker behavior Notes: - gutter markers are visual indicators only and do not pretend to be clickable - the existing CodeLens-based navigation remains unchanged - the same analysis result is reused for both CodeLens and gutter rendering --- README.md | 5 +- assets/implementation-dark.svg | 6 ++ assets/implementation-light.svg | 6 ++ assets/override-dark.svg | 6 ++ assets/override-light.svg | 6 ++ package.json | 5 ++ src/extension.ts | 73 ++++++++++++++++++----- src/gutterManager.ts | 101 ++++++++++++++++++++++++++++++++ 8 files changed, 193 insertions(+), 15 deletions(-) create mode 100644 assets/implementation-dark.svg create mode 100644 assets/implementation-light.svg create mode 100644 assets/override-dark.svg create mode 100644 assets/override-light.svg create mode 100644 src/gutterManager.ts diff --git a/README.md b/README.md index ecd7f0f..7e75db4 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 `true` | | `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..fcc4b5d 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": true, + "description": "Show gutter icons for overridden methods and methods implemented in subclasses." } } } diff --git a/src/extension.ts b/src/extension.ts index 84fc369..c8fb6c6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,12 +2,16 @@ import * as vscode from 'vscode'; import { OverrideDetector } from './overrideDetector'; import { OverrideCodeLensProvider } from './codeLensProvider'; import { SubclassCache, ReferenceClassificationCache } from './caching'; +import { OverrideGutterManager } from './gutterManager'; export function activate(context: vscode.ExtensionContext) { const detector = new OverrideDetector(); const codeLensProvider = new OverrideCodeLensProvider(); + const gutterManager = new OverrideGutterManager(context.extensionUri); + + context.subscriptions.push(gutterManager); // Register CodeLens Provider context.subscriptions.push( @@ -25,25 +29,45 @@ 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(); + 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(); + 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); + }).catch(error => { + if (editor === activeEditor) { + codeLensProvider.updateResults([]); + gutterManager.update(editor, []); + } + + console.error('Error updating override marks:', error); + }); }, delay); }; @@ -55,6 +79,8 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.window.onDidChangeActiveTextEditor(editor => { activeEditor = editor; + gutterManager.clear(); + if (editor) { triggerUpdate(); } @@ -76,6 +102,25 @@ 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', true); + + if (!gutterIconsEnabled) { + gutterManager.clear(); + } + } + + if (activeEditor?.document.languageId === 'python') { + triggerUpdate(); + } else { + gutterManager.clear(); + } }) ); diff --git a/src/gutterManager.ts b/src/gutterManager.ts new file mode 100644 index 0000000..8b93215 --- /dev/null +++ b/src/gutterManager.ts @@ -0,0 +1,101 @@ +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, + hoverMessage: `Overrides ${item.parentMethodName}` + }); + } + + if (item.type === 'implementation' && item.childMethods && item.childMethods.length > 0) { + implementationDecorations.push({ + range: item.range, + hoverMessage: this.getImplementationHoverMessage(item.childMethods.length) + }); + } + } + + 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', true); + } + + private clearEditor(editor: vscode.TextEditor): void { + editor.setDecorations(this.overrideDecorationType, []); + editor.setDecorations(this.implementationDecorationType, []); + } + + private getImplementationHoverMessage(count: number): string { + return count === 1 + ? 'Implemented in 1 subclass' + : `Implemented in ${count} subclasses`; + } +} From ec05929591269720cac6368494dbc9659c8ec56f Mon Sep 17 00:00:00 2001 From: showpiecep Date: Sun, 19 Apr 2026 17:48:12 +0300 Subject: [PATCH 2/3] feat: add dedicated hover provider for override gutter markers Introduce a dedicated HoverProvider for Python override and implementation markers so hover text is shown once on the method line instead of being duplicated by both decoration hover messages and editor hover rendering. Changes included: - add OverrideHoverProvider to build hover text from existing OverrideItem results - register the hover provider in the extension lifecycle - keep hover state in sync with the active editor and analysis updates - clear hover state when the editor changes or when results are reset - remove hoverMessage from gutter decorations to avoid duplicate tooltip content - respect pythonOverrideMark.gutterIcons.enabled when providing hover content Notes: - gutter icons remain visual markers only - hover is now provided through the editor hover system on the method line - existing CodeLens navigation behavior remains unchanged --- src/extension.ts | 11 ++++++- src/gutterManager.ts | 12 ++------ src/hoverProvider.ts | 69 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 src/hoverProvider.ts diff --git a/src/extension.ts b/src/extension.ts index c8fb6c6..de48a9d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,7 @@ 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) { @@ -10,12 +11,14 @@ 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; @@ -33,11 +36,13 @@ export function activate(context: vscode.ExtensionContext) { if (!editor) { gutterManager.clear(); + hoverProvider.updateResults(undefined, []); return; } if (editor.document.languageId !== 'python') { gutterManager.clear(); + hoverProvider.updateResults(undefined, []); return; } @@ -60,10 +65,12 @@ export function activate(context: vscode.ExtensionContext) { 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); @@ -80,6 +87,7 @@ export function activate(context: vscode.ExtensionContext) { vscode.window.onDidChangeActiveTextEditor(editor => { activeEditor = editor; gutterManager.clear(); + hoverProvider.updateResults(undefined, []); if (editor) { triggerUpdate(); @@ -120,6 +128,7 @@ export function activate(context: vscode.ExtensionContext) { triggerUpdate(); } else { gutterManager.clear(); + hoverProvider.updateResults(undefined, []); } }) ); diff --git a/src/gutterManager.ts b/src/gutterManager.ts index 8b93215..670bce5 100644 --- a/src/gutterManager.ts +++ b/src/gutterManager.ts @@ -54,15 +54,13 @@ export class OverrideGutterManager implements vscode.Disposable { for (const item of items) { if (item.type === 'override' && item.parentMethodName) { overrideDecorations.push({ - range: item.range, - hoverMessage: `Overrides ${item.parentMethodName}` + range: item.range }); } if (item.type === 'implementation' && item.childMethods && item.childMethods.length > 0) { implementationDecorations.push({ - range: item.range, - hoverMessage: this.getImplementationHoverMessage(item.childMethods.length) + range: item.range }); } } @@ -92,10 +90,4 @@ export class OverrideGutterManager implements vscode.Disposable { editor.setDecorations(this.overrideDecorationType, []); editor.setDecorations(this.implementationDecorationType, []); } - - private getImplementationHoverMessage(count: number): string { - return count === 1 - ? 'Implemented in 1 subclass' - : `Implemented in ${count} subclasses`; - } } diff --git a/src/hoverProvider.ts b/src/hoverProvider.ts new file mode 100644 index 0000000..bad2dc7 --- /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', true); + } +} From 2ff5ec576efc937309c55d95d4d7fb611ef446b9 Mon Sep 17 00:00:00 2001 From: showpiecep Date: Fri, 8 May 2026 18:57:01 +0300 Subject: [PATCH 3/3] feat: disable gutter icons by default Update the gutter icon configuration so the feature is hidden by default and must be explicitly enabled by the user. Changes included: - change pythonOverrideMark.gutterIcons.enabled default to false - align runtime fallback values with the contributed configuration default - update README to document the new default behavior Notes: - CodeLens remains available by default - gutter markers can still be enabled explicitly through settings - this change avoids changing breakpoint-related gutter behavior unless the user opts in --- README.md | 2 +- package.json | 4 ++-- src/extension.ts | 2 +- src/gutterManager.ts | 2 +- src/hoverProvider.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7e75db4..3ec5c3b 100644 --- a/README.md +++ b/README.md @@ -46,7 +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 `true` | +| `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 diff --git a/package.json b/package.json index fcc4b5d..331daa6 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,8 @@ }, "pythonOverrideMark.gutterIcons.enabled": { "type": "boolean", - "default": true, - "description": "Show gutter icons for overridden methods and methods implemented in subclasses." + "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 de48a9d..cb88bc5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -117,7 +117,7 @@ export function activate(context: vscode.ExtensionContext) { } if (event.affectsConfiguration('pythonOverrideMark.gutterIcons.enabled')) { - const gutterIconsEnabled = vscode.workspace.getConfiguration('pythonOverrideMark').get('gutterIcons.enabled', true); + const gutterIconsEnabled = vscode.workspace.getConfiguration('pythonOverrideMark').get('gutterIcons.enabled', false); if (!gutterIconsEnabled) { gutterManager.clear(); diff --git a/src/gutterManager.ts b/src/gutterManager.ts index 670bce5..df42fbb 100644 --- a/src/gutterManager.ts +++ b/src/gutterManager.ts @@ -83,7 +83,7 @@ export class OverrideGutterManager implements vscode.Disposable { } private isEnabled(): boolean { - return vscode.workspace.getConfiguration('pythonOverrideMark').get('gutterIcons.enabled', true); + return vscode.workspace.getConfiguration('pythonOverrideMark').get('gutterIcons.enabled', false); } private clearEditor(editor: vscode.TextEditor): void { diff --git a/src/hoverProvider.ts b/src/hoverProvider.ts index bad2dc7..523e300 100644 --- a/src/hoverProvider.ts +++ b/src/hoverProvider.ts @@ -64,6 +64,6 @@ export class OverrideHoverProvider implements vscode.HoverProvider { } private isEnabled(): boolean { - return vscode.workspace.getConfiguration('pythonOverrideMark').get('gutterIcons.enabled', true); + return vscode.workspace.getConfiguration('pythonOverrideMark').get('gutterIcons.enabled', false); } }