mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-08 15:55:15 +01:00
Merge pull request #206282 from microsoft/tyriar/reveal_link
Open detected links: Reveal source in terminal and timeout to use all links
This commit is contained in:
@@ -474,6 +474,11 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.terminal-range-highlight {
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.terminal-command-guide {
|
||||
left: 0;
|
||||
border: 1.5px solid #ffffff;
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/termi
|
||||
import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal';
|
||||
import { IRegisterContributedProfileArgs, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfiguration, ITerminalFont, ITerminalProcessExtHostProxy, ITerminalProcessInfo } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { ISimpleSelectedSuggestion } from 'vs/workbench/services/suggest/browser/simpleSuggestWidget';
|
||||
import type { IMarker, ITheme, Terminal as RawXtermTerminal } from '@xterm/xterm';
|
||||
import type { IMarker, ITheme, Terminal as RawXtermTerminal, IBufferRange } from '@xterm/xterm';
|
||||
import { ScrollPosition } from 'vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { GroupIdentifier } from 'vs/workbench/common/editor';
|
||||
@@ -113,13 +113,17 @@ export interface IMarkTracker {
|
||||
selectToNextMark(): void;
|
||||
selectToPreviousLine(): void;
|
||||
selectToNextLine(): void;
|
||||
clearMarker(): void;
|
||||
clear(): void;
|
||||
scrollToClosestMarker(startMarkerId: string, endMarkerId?: string, highlight?: boolean | undefined): void;
|
||||
|
||||
scrollToLine(line: number, position: ScrollPosition): void;
|
||||
revealCommand(command: ITerminalCommand, position?: ScrollPosition): void;
|
||||
revealRange(range: IBufferRange): void;
|
||||
registerTemporaryDecoration(marker: IMarker, endMarker: IMarker | undefined, showOutline: boolean): void;
|
||||
showCommandGuide(command: ITerminalCommand | undefined): void;
|
||||
|
||||
saveScrollState(): void;
|
||||
restoreScrollState(): void;
|
||||
}
|
||||
|
||||
export interface ITerminalGroup {
|
||||
|
||||
@@ -7,12 +7,14 @@ import { coalesce } from 'vs/base/common/arrays';
|
||||
import { Disposable, DisposableStore, MutableDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IMarkTracker } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { ITerminalCapabilityStore, ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
|
||||
import type { Terminal, IMarker, ITerminalAddon, IDecoration } from '@xterm/xterm';
|
||||
import type { Terminal, IMarker, ITerminalAddon, IDecoration, IBufferRange } from '@xterm/xterm';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
|
||||
import { getWindow } from 'vs/base/browser/dom';
|
||||
import { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { TerminalSettingId } from 'vs/platform/terminal/common/terminal';
|
||||
|
||||
enum Boundary {
|
||||
Top,
|
||||
@@ -24,6 +26,13 @@ export const enum ScrollPosition {
|
||||
Middle
|
||||
}
|
||||
|
||||
interface IScrollToMarkerOptions {
|
||||
hideDecoration?: boolean;
|
||||
/** Scroll even if the line is within the viewport */
|
||||
forceScroll?: boolean;
|
||||
bufferRange?: IBufferRange;
|
||||
}
|
||||
|
||||
export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITerminalAddon {
|
||||
private _currentMarker: IMarker | Boundary = Boundary.Bottom;
|
||||
private _selectionStart: IMarker | Boundary | null = null;
|
||||
@@ -43,6 +52,7 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
|
||||
|
||||
constructor(
|
||||
private readonly _capabilities: ITerminalCapabilityStore,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IThemeService private readonly _themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
@@ -91,7 +101,7 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
|
||||
return undefined;
|
||||
}
|
||||
|
||||
clearMarker(): void {
|
||||
clear(): void {
|
||||
// Clear the current marker so successive focus/selection actions are performed from the
|
||||
// bottom of the buffer
|
||||
this._currentMarker = Boundary.Bottom;
|
||||
@@ -219,16 +229,20 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
|
||||
}
|
||||
}
|
||||
|
||||
private _scrollToMarker(start: IMarker | number, position: ScrollPosition, end?: IMarker | number, hideDecoration?: boolean): void {
|
||||
private _scrollToMarker(start: IMarker | number, position: ScrollPosition, end?: IMarker | number, options?: IScrollToMarkerOptions): void {
|
||||
if (!this._terminal) {
|
||||
return;
|
||||
}
|
||||
if (!this._isMarkerInViewport(this._terminal, start)) {
|
||||
if (!this._isMarkerInViewport(this._terminal, start) || options?.forceScroll) {
|
||||
const line = this.getTargetScrollLine(toLineIndex(start), position);
|
||||
this._terminal.scrollToLine(line);
|
||||
}
|
||||
if (!hideDecoration) {
|
||||
this.registerTemporaryDecoration(start, end, true);
|
||||
if (!options?.hideDecoration) {
|
||||
if (options?.bufferRange) {
|
||||
this._highlightBufferRange(options.bufferRange);
|
||||
} else {
|
||||
this.registerTemporaryDecoration(start, end, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +274,19 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
|
||||
);
|
||||
}
|
||||
|
||||
revealRange(range: IBufferRange): void {
|
||||
this._scrollToMarker(
|
||||
range.start.y - 1,
|
||||
ScrollPosition.Middle,
|
||||
range.end.y - 1,
|
||||
{
|
||||
bufferRange: range,
|
||||
// Ensure scroll shows the line when sticky scroll is enabled
|
||||
forceScroll: !!this._configurationService.getValue(TerminalSettingId.StickyScrollEnabled)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
showCommandGuide(command: ITerminalCommand | undefined): void {
|
||||
if (!this._terminal) {
|
||||
return;
|
||||
@@ -314,6 +341,50 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private _scrollState: { viewportY: number } | undefined;
|
||||
|
||||
saveScrollState(): void {
|
||||
this._scrollState = { viewportY: this._terminal?.buffer.active.viewportY ?? 0 };
|
||||
}
|
||||
|
||||
restoreScrollState(): void {
|
||||
if (this._scrollState && this._terminal) {
|
||||
this._terminal.scrollToLine(this._scrollState.viewportY);
|
||||
this._scrollState = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _highlightBufferRange(range: IBufferRange): void {
|
||||
if (!this._terminal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._resetNavigationDecorations();
|
||||
const startLine = range.start.y;
|
||||
const decorationCount = range.end.y - range.start.y + 1;
|
||||
for (let i = 0; i < decorationCount; i++) {
|
||||
const decoration = this._terminal.registerDecoration({
|
||||
marker: this._createMarkerForOffset(startLine - 1, i),
|
||||
x: range.start.x - 1,
|
||||
width: (range.end.x - 1) - (range.start.x - 1) + 1,
|
||||
overviewRulerOptions: undefined
|
||||
});
|
||||
if (decoration) {
|
||||
this._navigationDecorations?.push(decoration);
|
||||
let renderedElement: HTMLElement | undefined;
|
||||
|
||||
decoration.onRender(element => {
|
||||
if (!renderedElement) {
|
||||
renderedElement = element;
|
||||
element.classList.add('terminal-range-highlight');
|
||||
}
|
||||
});
|
||||
decoration.onDispose(() => { this._navigationDecorations = this._navigationDecorations?.filter(d => d !== decoration); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerTemporaryDecoration(marker: IMarker | number, endMarker: IMarker | number | undefined, showOutline: boolean): void {
|
||||
if (!this._terminal) {
|
||||
return;
|
||||
@@ -373,7 +444,7 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
|
||||
}
|
||||
|
||||
getTargetScrollLine(line: number, position: ScrollPosition): number {
|
||||
// Middle is treated at 1/4 of the viewport's size because context below is almost always
|
||||
// Middle is treated as 1/4 of the viewport's size because context below is almost always
|
||||
// more important than context above in the terminal.
|
||||
if (this._terminal && position === ScrollPosition.Middle) {
|
||||
return Math.max(line - Math.floor(this._terminal.rows / 4), 0);
|
||||
@@ -397,7 +468,7 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
|
||||
return;
|
||||
}
|
||||
const endMarker = endMarkerId ? detectionCapability.getMark(endMarkerId) : startMarker;
|
||||
this._scrollToMarker(startMarker, ScrollPosition.Top, endMarker, !highlight);
|
||||
this._scrollToMarker(startMarker, ScrollPosition.Top, endMarker, { hideDecoration: !highlight });
|
||||
}
|
||||
|
||||
selectToPreviousMark(): void {
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ class TerminalLinkContribution extends DisposableStore implements ITerminalContr
|
||||
});
|
||||
}
|
||||
const links = await this._getLinks();
|
||||
return await this._terminalLinkQuickpick.show(links);
|
||||
return await this._terminalLinkQuickpick.show(this._instance, links);
|
||||
}
|
||||
|
||||
private async _getLinks(): Promise<{ viewport: IDetectedLinks; all: Promise<IDetectedLinks> }> {
|
||||
|
||||
@@ -8,13 +8,13 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { localize } from 'vs/nls';
|
||||
import { QuickPickItem, IQuickInputService, IQuickPickItem, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { IDetectedLinks } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager';
|
||||
import { TerminalLinkQuickPickEvent } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { TerminalLinkQuickPickEvent, type IDetachedTerminalInstance, type ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import type { ILink } from '@xterm/xterm';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
|
||||
import { AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
|
||||
import type { TerminalLink } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLink';
|
||||
import { Sequencer } from 'vs/base/common/async';
|
||||
import { Sequencer, timeout } from 'vs/base/common/async';
|
||||
import { EditorViewState } from 'vs/workbench/browser/quickaccess';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IHistoryService } from 'vs/workbench/services/history/common/history';
|
||||
@@ -30,6 +30,8 @@ export class TerminalLinkQuickpick extends DisposableStore {
|
||||
private readonly _editorSequencer = new Sequencer();
|
||||
private readonly _editorViewState: EditorViewState;
|
||||
|
||||
private _instance: ITerminalInstance | IDetachedTerminalInstance | undefined;
|
||||
|
||||
private readonly _onDidRequestMoreLinks = this.add(new Emitter<void>());
|
||||
readonly onDidRequestMoreLinks = this._onDidRequestMoreLinks.event;
|
||||
|
||||
@@ -45,12 +47,20 @@ export class TerminalLinkQuickpick extends DisposableStore {
|
||||
this._editorViewState = new EditorViewState(_editorService);
|
||||
}
|
||||
|
||||
async show(links: { viewport: IDetectedLinks; all: Promise<IDetectedLinks> }): Promise<void> {
|
||||
async show(instance: ITerminalInstance | IDetachedTerminalInstance, links: { viewport: IDetectedLinks; all: Promise<IDetectedLinks> }): Promise<void> {
|
||||
this._instance = instance;
|
||||
|
||||
// Allow all links a small amount of time to elapse to finish, if this is not done in this
|
||||
// time they will be loaded upon the first filter.
|
||||
const result = await Promise.race([links.all, timeout(500)]);
|
||||
const usingAllLinks = typeof result === 'object';
|
||||
const resolvedLinks = usingAllLinks ? result : links.viewport;
|
||||
|
||||
// Get raw link picks
|
||||
const wordPicks = links.viewport.wordLinks ? await this._generatePicks(links.viewport.wordLinks) : undefined;
|
||||
const filePicks = links.viewport.fileLinks ? await this._generatePicks(links.viewport.fileLinks) : undefined;
|
||||
const folderPicks = links.viewport.folderLinks ? await this._generatePicks(links.viewport.folderLinks) : undefined;
|
||||
const webPicks = links.viewport.webLinks ? await this._generatePicks(links.viewport.webLinks) : undefined;
|
||||
const wordPicks = resolvedLinks.wordLinks ? await this._generatePicks(resolvedLinks.wordLinks) : undefined;
|
||||
const filePicks = resolvedLinks.fileLinks ? await this._generatePicks(resolvedLinks.fileLinks) : undefined;
|
||||
const folderPicks = resolvedLinks.folderLinks ? await this._generatePicks(resolvedLinks.folderLinks) : undefined;
|
||||
const webPicks = resolvedLinks.webLinks ? await this._generatePicks(resolvedLinks.webLinks) : undefined;
|
||||
|
||||
const picks: LinkQuickPickItem[] = [];
|
||||
if (webPicks) {
|
||||
@@ -84,36 +94,38 @@ export class TerminalLinkQuickpick extends DisposableStore {
|
||||
// ASAP with only the viewport entries.
|
||||
let accepted = false;
|
||||
const disposables = new DisposableStore();
|
||||
disposables.add(Event.once(pick.onDidChangeValue)(async () => {
|
||||
const allLinks = await links.all;
|
||||
if (accepted) {
|
||||
return;
|
||||
}
|
||||
const wordIgnoreLinks = [...(allLinks.fileLinks ?? []), ...(allLinks.folderLinks ?? []), ...(allLinks.webLinks ?? [])];
|
||||
if (!usingAllLinks) {
|
||||
disposables.add(Event.once(pick.onDidChangeValue)(async () => {
|
||||
const allLinks = await links.all;
|
||||
if (accepted) {
|
||||
return;
|
||||
}
|
||||
const wordIgnoreLinks = [...(allLinks.fileLinks ?? []), ...(allLinks.folderLinks ?? []), ...(allLinks.webLinks ?? [])];
|
||||
|
||||
const wordPicks = allLinks.wordLinks ? await this._generatePicks(allLinks.wordLinks, wordIgnoreLinks) : undefined;
|
||||
const filePicks = allLinks.fileLinks ? await this._generatePicks(allLinks.fileLinks) : undefined;
|
||||
const folderPicks = allLinks.folderLinks ? await this._generatePicks(allLinks.folderLinks) : undefined;
|
||||
const webPicks = allLinks.webLinks ? await this._generatePicks(allLinks.webLinks) : undefined;
|
||||
const picks: LinkQuickPickItem[] = [];
|
||||
if (webPicks) {
|
||||
picks.push({ type: 'separator', label: localize('terminal.integrated.urlLinks', "Url") });
|
||||
picks.push(...webPicks);
|
||||
}
|
||||
if (filePicks) {
|
||||
picks.push({ type: 'separator', label: localize('terminal.integrated.localFileLinks', "File") });
|
||||
picks.push(...filePicks);
|
||||
}
|
||||
if (folderPicks) {
|
||||
picks.push({ type: 'separator', label: localize('terminal.integrated.localFolderLinks', "Folder") });
|
||||
picks.push(...folderPicks);
|
||||
}
|
||||
if (wordPicks) {
|
||||
picks.push({ type: 'separator', label: localize('terminal.integrated.searchLinks', "Workspace Search") });
|
||||
picks.push(...wordPicks);
|
||||
}
|
||||
pick.items = picks;
|
||||
}));
|
||||
const wordPicks = allLinks.wordLinks ? await this._generatePicks(allLinks.wordLinks, wordIgnoreLinks) : undefined;
|
||||
const filePicks = allLinks.fileLinks ? await this._generatePicks(allLinks.fileLinks) : undefined;
|
||||
const folderPicks = allLinks.folderLinks ? await this._generatePicks(allLinks.folderLinks) : undefined;
|
||||
const webPicks = allLinks.webLinks ? await this._generatePicks(allLinks.webLinks) : undefined;
|
||||
const picks: LinkQuickPickItem[] = [];
|
||||
if (webPicks) {
|
||||
picks.push({ type: 'separator', label: localize('terminal.integrated.urlLinks', "Url") });
|
||||
picks.push(...webPicks);
|
||||
}
|
||||
if (filePicks) {
|
||||
picks.push({ type: 'separator', label: localize('terminal.integrated.localFileLinks', "File") });
|
||||
picks.push(...filePicks);
|
||||
}
|
||||
if (folderPicks) {
|
||||
picks.push({ type: 'separator', label: localize('terminal.integrated.localFolderLinks', "Folder") });
|
||||
picks.push(...folderPicks);
|
||||
}
|
||||
if (wordPicks) {
|
||||
picks.push({ type: 'separator', label: localize('terminal.integrated.searchLinks', "Workspace Search") });
|
||||
picks.push(...wordPicks);
|
||||
}
|
||||
pick.items = picks;
|
||||
}));
|
||||
}
|
||||
|
||||
disposables.add(pick.onDidChangeActive(async () => {
|
||||
const [item] = pick.activeItems;
|
||||
@@ -122,6 +134,17 @@ export class TerminalLinkQuickpick extends DisposableStore {
|
||||
|
||||
return new Promise(r => {
|
||||
disposables.add(pick.onDidHide(({ reason }) => {
|
||||
|
||||
// Restore terminal scroll state
|
||||
if (this._terminalScrollStateSaved) {
|
||||
const markTracker = this._instance?.xterm?.markTracker;
|
||||
if (markTracker) {
|
||||
markTracker.restoreScrollState();
|
||||
markTracker.clear();
|
||||
this._terminalScrollStateSaved = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore view state upon cancellation if we changed it
|
||||
// but only when the picker was closed via explicit user
|
||||
// gesture and not e.g. when focus was lost because that
|
||||
@@ -208,11 +231,18 @@ export class TerminalLinkQuickpick extends DisposableStore {
|
||||
}
|
||||
|
||||
private _previewItem(item: ITerminalLinkQuickPickItem | IQuickPickItem) {
|
||||
if (!item || !('link' in item) || !item.link || !('uri' in item.link) || !item.link.uri) {
|
||||
if (!item || !('link' in item) || !item.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Any link can be previewed in the termninal
|
||||
const link = item.link;
|
||||
this._previewItemInTerminal(link);
|
||||
|
||||
if (!('uri' in link) || !link.uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (link.type !== TerminalBuiltinLinkType.LocalFile) {
|
||||
return;
|
||||
}
|
||||
@@ -223,6 +253,10 @@ export class TerminalLinkQuickpick extends DisposableStore {
|
||||
return;
|
||||
}
|
||||
|
||||
this._previewItemInEditor(link);
|
||||
}
|
||||
|
||||
private _previewItemInEditor(link: TerminalLink) {
|
||||
const linkSuffix = link.parsedLink ? link.parsedLink.suffix : getLinkSuffix(link.text);
|
||||
const selection = linkSuffix?.row === undefined ? undefined : {
|
||||
startLineNumber: linkSuffix.row ?? 1,
|
||||
@@ -245,6 +279,19 @@ export class TerminalLinkQuickpick extends DisposableStore {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _terminalScrollStateSaved: boolean = false;
|
||||
private _previewItemInTerminal(link: ILink) {
|
||||
const xterm = this._instance?.xterm;
|
||||
if (!xterm) {
|
||||
return;
|
||||
}
|
||||
if (!this._terminalScrollStateSaved) {
|
||||
xterm.markTracker.saveScrollState();
|
||||
this._terminalScrollStateSaved = true;
|
||||
}
|
||||
xterm.markTracker.revealRange(link.range);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITerminalLinkQuickPickItem extends IQuickPickItem {
|
||||
|
||||
Reference in New Issue
Block a user