diff --git a/src/vs/editor/contrib/codelens/browser/codelensController.ts b/src/vs/editor/contrib/codelens/browser/codelensController.ts index 4fd3619fb51c9..ffb6968e9ddf5 100644 --- a/src/vs/editor/contrib/codelens/browser/codelensController.ts +++ b/src/vs/editor/contrib/codelens/browser/codelensController.ts @@ -24,6 +24,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { Emitter, Event } from 'vs/base/common/event'; export class CodeLensContribution implements IEditorContribution { @@ -41,6 +42,8 @@ export class CodeLensContribution implements IEditorContribution { private _getCodeLensModelPromise: CancelablePromise | undefined; private _oldCodeLensModels = new DisposableStore(); private _currentCodeLensModel: CodeLensModel | undefined; + private _onDidChangeCodeLensModel: Emitter = new Emitter(); + public readonly onDidChangeCodeLensModel: Event = this._onDidChangeCodeLensModel.event; private _resolveCodeLensesPromise: CancelablePromise | undefined; constructor( @@ -76,6 +79,7 @@ export class CodeLensContribution implements IEditorContribution { this._disposables.dispose(); this._oldCodeLensModels.dispose(); this._currentCodeLensModel?.dispose(); + this._onDidChangeCodeLensModel.fire(); } private _getLayoutInfo() { @@ -123,6 +127,7 @@ export class CodeLensContribution implements IEditorContribution { this._localToDispose.clear(); this._oldCodeLensModels.clear(); this._currentCodeLensModel?.dispose(); + this._onDidChangeCodeLensModel.fire(); } private _onModelChange(): void { @@ -176,6 +181,7 @@ export class CodeLensContribution implements IEditorContribution { this._oldCodeLensModels.add(this._currentCodeLensModel); } this._currentCodeLensModel = result; + this._onDidChangeCodeLensModel.fire(); // cache model to reduce flicker this._codeLensCache.put(model, result); diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts index fb7adfb064a0f..980972a73d852 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts @@ -26,6 +26,9 @@ import { ILanguageConfigurationService } from 'vs/editor/common/languages/langua import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import * as dom from 'vs/base/browser/dom'; import { StickyRange } from 'vs/editor/contrib/stickyScroll/browser/stickyScrollElement'; +import { CodeLensContribution } from 'vs/editor/contrib/codelens/browser/codelensController'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { INotificationService } from 'vs/platform/notification/common/notification'; interface CustomMouseEvent { detail: string; @@ -76,7 +79,9 @@ export class StickyScrollController extends Disposable implements IEditorContrib @IInstantiationService private readonly _instaService: IInstantiationService, @ILanguageConfigurationService _languageConfigurationService: ILanguageConfigurationService, @ILanguageFeatureDebounceService _languageFeatureDebounceService: ILanguageFeatureDebounceService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ICommandService private readonly _commandService: ICommandService, + @INotificationService private readonly _notificationService: INotificationService ) { super(); this._stickyScrollWidget = new StickyScrollWidget(this._editor); @@ -84,7 +89,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._register(this._stickyScrollWidget); this._register(this._stickyLineCandidateProvider); - this._widgetState = new StickyScrollWidgetState([], 0); + this._widgetState = new StickyScrollWidgetState([], 0, undefined); this._readConfiguration(); this._register(this._editor.onDidChangeConfiguration(e => { if (e.hasChanged(EditorOption.stickyScroll)) { @@ -104,7 +109,6 @@ export class StickyScrollController extends Disposable implements IEditorContrib if (this._positionRevealed === false && height === 0) { this._focusedStickyElementIndex = -1; this.focus(); - } // In all other casees, dispose the focus on the sticky scroll else { @@ -278,9 +282,16 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._revealPosition({ lineNumber: this._stickyScrollWidget.hoverOnLine, column: 1 }); } this._instaService.invokeFunction(goToDefinitionWithLocation, e, this._editor as IActiveCodeEditor, { uri: this._editor.getModel()!.uri, range: this._stickyRangeProjectedOnEditor! }); - } else if (!e.isRightClick) { // Normal click + if (e.target?.element?.getAttribute('role') === 'button') { + // Click CodeLens button + const command = this._stickyScrollWidget.getCommand(e.target.element as HTMLLinkElement); + if (command) { + this._commandService.executeCommand(command.id, ...(command.arguments || [])).catch(err => this._notificationService.error(err)); + } + return; + } if (this._focused) { this._disposeFocusStickyScrollStore(); } @@ -312,6 +323,18 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._sessionStore.add(this._editor.onDidLayoutChange(() => this._onDidResize())); this._sessionStore.add(this._editor.onDidChangeModelTokens((e) => this._onTokensChange(e))); this._sessionStore.add(this._stickyLineCandidateProvider.onDidChangeStickyScroll(() => this._renderStickyScroll())); + this._sessionStore.add(this._editor.onDidChangeConfiguration(e => { + if (e.hasChanged(EditorOption.codeLens)) { + this._renderStickyScroll(); + } + })); + + // When codeLensModel changed, add the listeners on the sticky scroll + const codeLensContribution = this._editor.getContribution(CodeLensContribution.ID); + if (codeLensContribution) { + this._sessionStore.add(codeLensContribution.onDidChangeCodeLensModel(() => this._renderStickyScroll())); + } + this._enabled = true; } @@ -426,7 +449,8 @@ export class StickyScrollController extends Disposable implements IEditorContrib } } } - return new StickyScrollWidgetState(lineNumbers, lastLineRelativePosition); + const codeLensContribution = this._editor.getContribution(CodeLensContribution.ID); + return new StickyScrollWidgetState(lineNumbers, lastLineRelativePosition, this._editor.getOption(EditorOption.codeLens) ? codeLensContribution?.getModel?.() : undefined); } override dispose(): void { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index 7b4a946f5f358..db302571009ac 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -15,11 +15,15 @@ import { Position } from 'vs/editor/common/core/position'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations'; import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; +import { CodeLensItem, CodeLensModel } from 'vs/editor/contrib/codelens/browser/codelens'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Command } from 'vs/editor/common/languages'; export class StickyScrollWidgetState { constructor( readonly lineNumbers: number[], - readonly lastLineRelativePosition: number + readonly lastLineRelativePosition: number, + readonly codeLensModel: CodeLensModel | undefined, ) { } } @@ -33,8 +37,10 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { private _lineNumbers: number[] = []; private _lastLineRelativePosition: number = 0; + private _codeLensModel: CodeLensModel | undefined = undefined; private _hoverOnLine: number = -1; private _hoverOnColumn: number = -1; + private readonly _commands = new Map(); constructor( private readonly _editor: ICodeEditor @@ -81,21 +87,43 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { this._lastLineRelativePosition = 0; this._lineNumbers = []; } + this._codeLensModel = state.codeLensModel; this._renderRootNode(); } private _renderRootNode(): void { - if (!this._editor._getViewModel()) { return; } - for (const [index, line] of this._lineNumbers.entries()) { - const childNode = this._renderChildNode(index, line); - this._rootDomNode.appendChild(childNode); + + let codeLensLineHeightCount = 0; + + // If there is no lineNumbers, check if there are codelens for the first line of editor's visible ranges + if (!this._lineNumbers.length) { + const firstLineNumberOfVisibleRanges = this._editor.getVisibleRanges()[0].startLineNumber; + const matchedCodelensStartLineNumber = this._getCodeLensStartLineNumber(firstLineNumberOfVisibleRanges); + + if (matchedCodelensStartLineNumber) { + const codeLensChildNode = this._renderCodeLensLine(matchedCodelensStartLineNumber); + if (codeLensChildNode) { + this._rootDomNode.appendChild(codeLensChildNode); + codeLensLineHeightCount += codeLensChildNode.clientHeight; + } + } + } else { + for (const [index, line] of this._lineNumbers.entries()) { + const codeLensChildNode = this._renderCodeLensLine(line); + if (codeLensChildNode) { + this._rootDomNode.appendChild(codeLensChildNode); + codeLensLineHeightCount += codeLensChildNode.clientHeight; + } + const childNode = this._renderChildNode(index, line); + this._rootDomNode.appendChild(childNode); + } } const editorLineHeight = this._editor.getOption(EditorOption.lineHeight); - const widgetHeight: number = this._lineNumbers.length * editorLineHeight + this._lastLineRelativePosition; + const widgetHeight: number = this._lineNumbers.length * editorLineHeight + codeLensLineHeightCount + this._lastLineRelativePosition; this._rootDomNode.style.display = widgetHeight > 0 ? 'block' : 'none'; this._rootDomNode.style.height = widgetHeight.toString() + 'px'; this._rootDomNode.setAttribute('role', 'list'); @@ -106,8 +134,132 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { } } - private _renderChildNode(index: number, line: number): HTMLDivElement { + private _renderCodeLensLine(lineNumber: number) { + this._commands.clear(); + + const codeLensItems = this._groupCodeLensModel(this._codeLensModel); + if (!codeLensItems?.length) { + return; + } + + const lineCodeLensItems = codeLensItems.find(cl => cl[0].symbol?.range?.startLineNumber === lineNumber); + if (!lineCodeLensItems?.length) { + return; + } + const child = document.createElement('div'); + const layoutInfo = this._editor.getLayoutInfo(); + const width = layoutInfo.width - layoutInfo.minimap.minimapCanvasOuterWidth - layoutInfo.verticalScrollbarWidth; + child.className = 'sticky-line-root'; + child.setAttribute('role', 'listitem'); + child.tabIndex = 0; + child.style.cssText = `width: ${width}px; z-index: 0;`; + const codeLensCont = document.createElement('div'); + codeLensCont.className = 'codelens-decoration'; + codeLensCont.style.cssText = `position: relative; left: ${layoutInfo.contentLeft}px;`; + child.appendChild(codeLensCont); + + // Convert codelens startColumn to space and use renderViewLine to render it + const viewModel = this._editor._getViewModel(); + const viewLineNumber = viewModel!.coordinatesConverter.convertModelPositionToViewPosition(new Position(lineNumber, 1)).lineNumber; + const lineRenderingData = viewModel!.getViewLineRenderingData(viewLineNumber); + const renderLineInput: RenderLineInput = new RenderLineInput( + true, + true, + '\u00a0'.repeat(lineCodeLensItems[0].symbol.range.startColumn - 1), + false, + true, + false, + 0, + lineRenderingData.tokens, + [], + lineRenderingData.tabSize, + 1, + 1, 1, 1, 500, 'none', true, true, null + ); + const sb = new StringBuilder(2000); + renderViewLine(renderLineInput, sb); + let newLine; + if (_ttPolicy) { + newLine = _ttPolicy.createHTML(sb.build() as string); + } else { + newLine = sb.build(); + } + const indentNodeChild = document.createElement('span'); + indentNodeChild.innerHTML = newLine as string; + this._editor.applyFontInfo(indentNodeChild); + codeLensCont.append(indentNodeChild); + + // Render codeLens commands + const children: HTMLElement[] = []; + lineCodeLensItems.forEach((lens, i) => { + if (lens?.symbol?.command) { + const title = renderLabelWithIcons(lens.symbol.command?.title?.trim()); + if (lens.symbol.command?.id) { + children.push(dom.$('a', { id: String(i), title: lens.symbol.command?.tooltip, role: 'button' }, ...title)); + this._commands.set(String(i), lens.symbol.command); + } else { + children.push(dom.$('span', { title: lens.symbol.command?.tooltip }, ...title)); + } + if (i + 1 < lineCodeLensItems.length) { + children.push(dom.$('span', undefined, '\u00a0|\u00a0')); + } + } + }); + codeLensCont.append(...children); + + return child; + } + + private _getCodeLensStartLineNumber(lineNumber: number) { + const codeLensItems = this._groupCodeLensModel(this._codeLensModel); + if (!codeLensItems?.length) { + return; + } + + const matchedCodeLens = codeLensItems.find(cl => { + const clRange = cl[0]?.symbol?.range; + + if (clRange) { + if (lineNumber >= clRange.startLineNumber && lineNumber <= clRange.endLineNumber) { + return true; + } + } + + return false; + }); + + return matchedCodeLens?.[0].symbol?.range?.startLineNumber; + } + + private _groupCodeLensModel(codeLensModel: CodeLensModel | undefined): CodeLensItem[][] { + if (!codeLensModel) { + return []; + } + + const maxLineNumber = this._editor.getModel()?.getLineCount() || 0; + const groups: CodeLensItem[][] = []; + let lastGroup: CodeLensItem[] | undefined; + + for (const symbol of codeLensModel.lenses) { + const line = symbol.symbol.range.startLineNumber; + if (line < 1 || line > maxLineNumber) { + // invalid code lens + continue; + } else if (lastGroup && lastGroup[lastGroup.length - 1].symbol.range.startLineNumber === line) { + // on same line as previous + lastGroup.push(symbol); + } else { + // on later line as previous + lastGroup = [symbol]; + groups.push(lastGroup); + } + } + + return groups; + } + + private _renderChildNode(index: number, line: number): HTMLDivElement { const child = document.createElement('div'); const viewModel = this._editor._getViewModel(); const viewLineNumber = viewModel!.coordinatesConverter.convertModelPositionToViewPosition(new Position(line, 1)).lineNumber; @@ -224,4 +376,8 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { preference: null }; } + + getCommand(link: HTMLLinkElement) { + return link.parentElement?.parentElement?.parentElement === this._rootDomNode ? this._commands.get(link.id) : undefined; + } }