Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
6 changes: 6 additions & 0 deletions src/vs/editor/contrib/codelens/browser/codelensController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -41,6 +42,8 @@ export class CodeLensContribution implements IEditorContribution {
private _getCodeLensModelPromise: CancelablePromise<CodeLensModel> | undefined;
private _oldCodeLensModels = new DisposableStore();
private _currentCodeLensModel: CodeLensModel | undefined;
private _onDidChangeCodeLensModel: Emitter<void> = new Emitter();
public readonly onDidChangeCodeLensModel: Event<void> = this._onDidChangeCodeLensModel.event;
private _resolveCodeLensesPromise: CancelablePromise<any> | undefined;

constructor(
Expand Down Expand Up @@ -76,6 +79,7 @@ export class CodeLensContribution implements IEditorContribution {
this._disposables.dispose();
this._oldCodeLensModels.dispose();
this._currentCodeLensModel?.dispose();
this._onDidChangeCodeLensModel.fire();
}

private _getLayoutInfo() {
Expand Down Expand Up @@ -123,6 +127,7 @@ export class CodeLensContribution implements IEditorContribution {
this._localToDispose.clear();
this._oldCodeLensModels.clear();
this._currentCodeLensModel?.dispose();
this._onDidChangeCodeLensModel.fire();
}

private _onModelChange(): void {
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -76,15 +79,17 @@ 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);
this._stickyLineCandidateProvider = new StickyLineCandidateProvider(this._editor, _languageFeaturesService, _languageConfigurationService);
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)) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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>(CodeLensContribution.ID);
if (codeLensContribution) {
this._sessionStore.add(codeLensContribution.onDidChangeCodeLensModel(() => this._renderStickyScroll()));
}

this._enabled = true;
}

Expand Down Expand Up @@ -426,7 +449,8 @@ export class StickyScrollController extends Disposable implements IEditorContrib
}
}
}
return new StickyScrollWidgetState(lineNumbers, lastLineRelativePosition);
const codeLensContribution = this._editor.getContribution<CodeLensContribution>(CodeLensContribution.ID);
return new StickyScrollWidgetState(lineNumbers, lastLineRelativePosition, this._editor.getOption(EditorOption.codeLens) ? codeLensContribution?.getModel?.() : undefined);
}

override dispose(): void {
Expand Down
170 changes: 163 additions & 7 deletions src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
import * as dom from 'vs/base/browser/dom';
import 'vs/css!./stickyScroll';
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,
) { }
}

Expand All @@ -31,8 +35,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<string, Command>();

constructor(
private readonly _editor: ICodeEditor
Expand Down Expand Up @@ -79,21 +85,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');
Expand All @@ -104,8 +132,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
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aiday-mar Is there a better way to compute the Code Lens's marginLeft?

Copy link
Copy Markdown
Contributor Author

@harbin1053020115 harbin1053020115 May 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As now I use renderViewLine to render spaces(according to the startColumn) before the Code Lens, which I think is not a lightweight solution.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VSCodeTriageBot Any one can handle this pull request?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@harbin1053020115 the team are in the middle of endgame, so I don't think this PR will get looked at until afterwards.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@harbin1053020115 the team are in the middle of endgame, so I don't think this PR will get looked at until afterwards.

@gjsjohnmurray You mean May 2023 Endgame(https://github.com/microsoft/vscode/issues/183504)?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, #183504

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, #183504

Got 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;
Expand Down Expand Up @@ -222,4 +374,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;
}
}