Skip to content
30 changes: 27 additions & 3 deletions extensions/git/src/fileSystemProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,26 @@ export class GitFileSystemProvider implements FileSystemProvider {
this.cache = cache;
}

private async getOrOpenRepository(uri: string | Uri): Promise<Repository | undefined> {
let repository = this.model.getRepository(uri);
if (repository) {
return repository;
}

// In case of the empty window, or the agent sessions window, no repositories are open
// so we need to explicitly open a repository before we can serve git content for the
// given git resource.
if (workspace.workspaceFolders === undefined || workspace.isAgentSessionsWorkspace) {
const fsPath = typeof uri === 'string' ? uri : fromGitUri(uri).path;
this.logger.info(`[GitFileSystemProvider][getOrOpenRepository] Opening repository for ${fsPath}`);

await this.model.openRepository(fsPath, true, true);
repository = this.model.getRepository(uri);
}

return repository;
}

watch(): Disposable {
return EmptyDisposable;
}
Expand All @@ -139,7 +159,11 @@ export class GitFileSystemProvider implements FileSystemProvider {
await this.model.isInitialized;

const { submoduleOf, path, ref } = fromGitUri(uri);
const repository = submoduleOf ? this.model.getRepository(submoduleOf) : this.model.getRepository(uri);

const repository = submoduleOf
? await this.getOrOpenRepository(submoduleOf)
: await this.getOrOpenRepository(uri);

if (!repository) {
this.logger.warn(`[GitFileSystemProvider][stat] Repository not found - ${uri.toString()}`);
throw FileSystemError.FileNotFound();
Expand Down Expand Up @@ -175,7 +199,7 @@ export class GitFileSystemProvider implements FileSystemProvider {
const { path, ref, submoduleOf } = fromGitUri(uri);

if (submoduleOf) {
const repository = this.model.getRepository(submoduleOf);
const repository = await this.getOrOpenRepository(submoduleOf);

if (!repository) {
throw FileSystemError.FileNotFound();
Expand All @@ -190,7 +214,7 @@ export class GitFileSystemProvider implements FileSystemProvider {
}
}

const repository = this.model.getRepository(uri);
const repository = await this.getOrOpenRepository(uri);

if (!repository) {
this.logger.warn(`[GitFileSystemProvider][readFile] Repository not found - ${uri.toString()}`);
Expand Down
47 changes: 45 additions & 2 deletions src/vs/base/common/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createSingleCallFunction } from './functional.js';
import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from './lifecycle.js';
import { LinkedList } from './linkedList.js';
import { IObservable, IObservableWithChange, IObserver } from './observable.js';
import { env } from './process.js';
import { StopWatch } from './stopwatch.js';
import { MicrotaskDelay } from './symbols.js';

Expand All @@ -31,6 +32,14 @@ const _enableSnapshotPotentialLeakWarning = false
// || Boolean("TRUE") // causes a linter warning so that it cannot be pushed
;


const _bufferLeakWarnCountThreshold = 100;
const _bufferLeakWarnTimeThreshold = 60_000; // 1 minute

function _isBufferLeakWarningEnabled(): boolean {
return !!env['VSCODE_DEV'];
}

/**
* An event with zero or one parameters that can be subscribed to. The event is a function itself.
*/
Expand Down Expand Up @@ -490,6 +499,7 @@ export namespace Event {
* returned event causes this utility to leak a listener on the original event.
*
* @param event The event source for the new event.
* @param debugName A name for this buffer, used in leak detection warnings.
* @param flushAfterTimeout Determines whether to flush the buffer after a timeout immediately or after a
* `setTimeout` when the first event listener is added.
* @param _buffer Internal: A source event array used for tests.
Expand All @@ -499,15 +509,46 @@ export namespace Event {
* // Start accumulating events, when the first listener is attached, flush
* // the event after a timeout such that multiple listeners attached before
* // the timeout would receive the event
* this.onInstallExtension = Event.buffer(service.onInstallExtension, true);
* this.onInstallExtension = Event.buffer(service.onInstallExtension, 'onInstallExtension', true);
* ```
*/
export function buffer<T>(event: Event<T>, flushAfterTimeout = false, _buffer: T[] = [], disposable?: DisposableStore): Event<T> {
export function buffer<T>(event: Event<T>, debugName: string, flushAfterTimeout = false, _buffer: T[] = [], disposable?: DisposableStore): Event<T> {
let buffer: T[] | null = _buffer.slice();

// Dev-only leak detection: track when buffer was created and warn
// if events accumulate without ever being consumed.
let bufferLeakWarningData: { stack: Stacktrace; timerId: ReturnType<typeof setTimeout>; warned: boolean } | undefined;
if (_isBufferLeakWarningEnabled()) {
bufferLeakWarningData = {
stack: Stacktrace.create(),
timerId: setTimeout(() => {
if (buffer && buffer.length > 0 && bufferLeakWarningData && !bufferLeakWarningData.warned) {
bufferLeakWarningData.warned = true;
console.warn(`[Event.buffer][${debugName}] potential LEAK detected: ${buffer.length} events buffered for ${_bufferLeakWarnTimeThreshold / 1000}s without being consumed. Buffered here:`);
bufferLeakWarningData.stack.print();
}
}, _bufferLeakWarnTimeThreshold),
warned: false
};
if (disposable) {
disposable.add(toDisposable(() => clearTimeout(bufferLeakWarningData!.timerId)));
}
}

const clearLeakWarningTimer = () => {
if (bufferLeakWarningData) {
clearTimeout(bufferLeakWarningData.timerId);
}
};

let listener: IDisposable | null = event(e => {
if (buffer) {
buffer.push(e);
if (_isBufferLeakWarningEnabled() && bufferLeakWarningData && !bufferLeakWarningData.warned && buffer.length >= _bufferLeakWarnCountThreshold) {
bufferLeakWarningData.warned = true;
console.warn(`[Event.buffer][${debugName}] potential LEAK detected: ${buffer.length} events buffered without being consumed. Buffered here:`);
bufferLeakWarningData.stack.print();
}
} else {
emitter.fire(e);
}
Expand All @@ -520,6 +561,7 @@ export namespace Event {
const flush = () => {
buffer?.forEach(e => emitter.fire(e));
buffer = null;
clearLeakWarningTimer();
};

const emitter = new Emitter<T>({
Expand Down Expand Up @@ -547,6 +589,7 @@ export namespace Event {
listener.dispose();
}
listener = null;
clearLeakWarningTimer();
}
});

Expand Down
4 changes: 2 additions & 2 deletions src/vs/base/parts/ipc/common/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1118,7 +1118,7 @@ export namespace ProxyChannel {
const mapEventNameToEvent = new Map<string, Event<unknown>>();
for (const key in handler) {
if (propertyIsEvent(key)) {
mapEventNameToEvent.set(key, Event.buffer(handler[key] as Event<unknown>, true, undefined, disposables));
mapEventNameToEvent.set(key, Event.buffer(handler[key] as Event<unknown>, key, true, undefined, disposables));
}
}

Expand All @@ -1137,7 +1137,7 @@ export namespace ProxyChannel {
}

if (propertyIsEvent(event)) {
mapEventNameToEvent.set(event, Event.buffer(handler[event] as Event<unknown>, true, undefined, disposables));
mapEventNameToEvent.set(event, Event.buffer(handler[event] as Event<unknown>, event, true, undefined, disposables));

return mapEventNameToEvent.get(event) as Event<T>;
}
Expand Down
6 changes: 3 additions & 3 deletions src/vs/base/test/common/event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,7 +1006,7 @@ suite('Event utils', () => {
const result: number[] = [];
const emitter = ds.add(new Emitter<number>());
const event = emitter.event;
const bufferedEvent = Event.buffer(event);
const bufferedEvent = Event.buffer(event, 'test');

emitter.fire(1);
emitter.fire(2);
Expand All @@ -1028,7 +1028,7 @@ suite('Event utils', () => {
const result: number[] = [];
const emitter = ds.add(new Emitter<number>());
const event = emitter.event;
const bufferedEvent = Event.buffer(event, true);
const bufferedEvent = Event.buffer(event, 'test', true);

emitter.fire(1);
emitter.fire(2);
Expand All @@ -1050,7 +1050,7 @@ suite('Event utils', () => {
const result: number[] = [];
const emitter = ds.add(new Emitter<number>());
const event = emitter.event;
const bufferedEvent = Event.buffer(event, false, [-2, -1, 0]);
const bufferedEvent = Event.buffer(event, 'test', false, [-2, -1, 0]);

emitter.fire(1);
emitter.fire(2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class NativeEditContext extends AbstractEditContext {
private readonly _editContext: EditContext;
private readonly _screenReaderSupport: ScreenReaderSupport;
private _previousEditContextSelection: OffsetRange = new OffsetRange(0, 0);
private _previousEditContextText: string = '';
private _editContextPrimarySelection: Selection = new Selection(1, 1, 1, 1);

// Overflow guard container
Expand Down Expand Up @@ -247,6 +248,19 @@ export class NativeEditContext extends AbstractEditContext {
}
}));
this._register(NativeEditContextRegistry.register(ownerID, this));
this._register(context.viewModel.model.onDidChangeContent((e) => {
let doChange = false;
for (const change of e.changes) {
if (change.range.startLineNumber <= this._editContextPrimarySelection.endLineNumber
&& change.range.endLineNumber >= this._editContextPrimarySelection.startLineNumber) {
doChange = true;
break;
}
}
if (doChange) {
this._updateEditContext();
}
}));
}

// --- Public methods ---
Expand Down Expand Up @@ -310,27 +324,17 @@ export class NativeEditContext extends AbstractEditContext {
}

public override onLinesChanged(e: ViewLinesChangedEvent): boolean {
this._updateEditContextOnLineChange(e.fromLineNumber, e.fromLineNumber + e.count - 1);
return true;
}

public override onLinesDeleted(e: ViewLinesDeletedEvent): boolean {
this._updateEditContextOnLineChange(e.fromLineNumber, e.toLineNumber);
return true;
}

public override onLinesInserted(e: ViewLinesInsertedEvent): boolean {
this._updateEditContextOnLineChange(e.fromLineNumber, e.toLineNumber);
return true;
}

private _updateEditContextOnLineChange(fromLineNumber: number, toLineNumber: number): void {
if (this._editContextPrimarySelection.endLineNumber < fromLineNumber || this._editContextPrimarySelection.startLineNumber > toLineNumber) {
return;
}
this._updateEditContext();
}

public override onScrollChanged(e: ViewScrollChangedEvent): boolean {
this._scrollLeft = e.scrollLeft;
this._scrollTop = e.scrollTop;
Expand Down Expand Up @@ -412,8 +416,15 @@ export class NativeEditContext extends AbstractEditContext {
if (!editContextState) {
return;
}
this._editContext.updateText(0, Number.MAX_SAFE_INTEGER, editContextState.text ?? ' ');
this._editContext.updateSelection(editContextState.selectionStartOffset, editContextState.selectionEndOffset);
const newText = editContextState.text ?? ' ';
if (newText !== this._previousEditContextText) {
this._editContext.updateText(0, this._previousEditContextText.length, newText);
this._previousEditContextText = newText;
}
if (editContextState.selectionStartOffset !== this._previousEditContextSelection.start ||
editContextState.selectionEndOffset !== this._previousEditContextSelection.endExclusive) {
this._editContext.updateSelection(editContextState.selectionStartOffset, editContextState.selectionEndOffset);
}
this._editContextPrimarySelection = editContextState.editContextPrimarySelection;
this._previousEditContextSelection = new OffsetRange(editContextState.selectionStartOffset, editContextState.selectionEndOffset);
}
Expand Down
1 change: 1 addition & 0 deletions src/vs/editor/browser/widget/codeEditor/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
position: relative;
overflow: visible;
-webkit-text-size-adjust: 100%;
text-spacing-trim: space-all;
color: var(--vscode-editor-foreground);
background-color: var(--vscode-editor-background);
overflow-wrap: initial;
Expand Down
12 changes: 12 additions & 0 deletions src/vs/editor/common/config/editorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2322,6 +2322,11 @@ export interface IEditorHoverOptions {
* Defaults to false.
*/
above?: boolean;
/**
* Should long line warning hovers be shown (tokenization skipped, rendering paused)?
* Defaults to true.
*/
showLongLineWarning?: boolean;
}

/**
Expand All @@ -2338,6 +2343,7 @@ class EditorHover extends BaseEditorOption<EditorOption.hover, IEditorHoverOptio
hidingDelay: 300,
sticky: true,
above: true,
showLongLineWarning: true,
};
super(
EditorOption.hover, 'hover', defaults,
Expand Down Expand Up @@ -2376,6 +2382,11 @@ class EditorHover extends BaseEditorOption<EditorOption.hover, IEditorHoverOptio
default: defaults.above,
description: nls.localize('hover.above', "Prefer showing hovers above the line, if there's space.")
},
'editor.hover.showLongLineWarning': {
type: 'boolean',
default: defaults.showLongLineWarning,
description: nls.localize('hover.showLongLineWarning', "Controls whether long line warning hovers are shown, such as when tokenization is skipped or rendering is paused.")
},
}
);
}
Expand All @@ -2391,6 +2402,7 @@ class EditorHover extends BaseEditorOption<EditorOption.hover, IEditorHoverOptio
sticky: boolean(input.sticky, this.defaultValue.sticky),
hidingDelay: EditorIntOption.clampedInt(input.hidingDelay, this.defaultValue.hidingDelay, 0, 600000),
above: boolean(input.above, this.defaultValue.above),
showLongLineWarning: boolean(input.showLongLineWarning, this.defaultValue.showLongLineWarning),
};
}
}
Expand Down
1 change: 1 addition & 0 deletions src/vs/editor/contrib/hover/browser/hoverActionIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export const INCREASE_HOVER_VERBOSITY_ACTION_LABEL = nls.localize({ key: 'increa
export const DECREASE_HOVER_VERBOSITY_ACTION_ID = 'editor.action.decreaseHoverVerbosityLevel';
export const DECREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID = 'editor.action.decreaseHoverVerbosityLevelFromAccessibleView';
export const DECREASE_HOVER_VERBOSITY_ACTION_LABEL = nls.localize({ key: 'decreaseHoverVerbosityLevel', comment: ['Label for action that will decrease the hover verbosity level.'] }, "Decrease Hover Verbosity Level");
export const HIDE_LONG_LINE_WARNING_HOVER_ACTION_ID = 'editor.action.hideLongLineWarningHover';
6 changes: 6 additions & 0 deletions src/vs/editor/contrib/hover/browser/hoverContribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { DecreaseHoverVerbosityLevel, GoToBottomHoverAction, GoToTopHoverAction,
import { EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from '../../../browser/editorExtensions.js';
import { editorHoverBorder } from '../../../../platform/theme/common/colorRegistry.js';
import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { HIDE_LONG_LINE_WARNING_HOVER_ACTION_ID } from './hoverActionIds.js';
import { HoverParticipantRegistry } from './hoverTypes.js';
import { MarkdownHoverParticipant } from './markdownHoverParticipant.js';
import { MarkerHoverParticipant } from './markerHoverParticipant.js';
Expand All @@ -33,6 +36,9 @@ registerEditorAction(IncreaseHoverVerbosityLevel);
registerEditorAction(DecreaseHoverVerbosityLevel);
HoverParticipantRegistry.register(MarkdownHoverParticipant);
HoverParticipantRegistry.register(MarkerHoverParticipant);
CommandsRegistry.registerCommand(HIDE_LONG_LINE_WARNING_HOVER_ACTION_ID, (accessor) => {
accessor.get(IConfigurationService).updateValue('editor.hover.showLongLineWarning', false);
});

// theming
registerThemingParticipant((theme, collector) => {
Expand Down
29 changes: 22 additions & 7 deletions src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { CancellationToken, CancellationTokenSource } from '../../../../base/com
import { IMarkdownString, isEmptyMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';
import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
import { DECREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID } from './hoverActionIds.js';
import { DECREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID, HIDE_LONG_LINE_WARNING_HOVER_ACTION_ID } from './hoverActionIds.js';
import { ICodeEditor } from '../../../browser/editorBrowser.js';
import { Position } from '../../../common/core/position.js';
import { Range } from '../../../common/core/range.js';
Expand Down Expand Up @@ -115,17 +115,32 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant<Markdow
const maxTokenizationLineLength = this._configurationService.getValue<number>('editor.maxTokenizationLineLength', {
overrideIdentifier: languageId
});
const showLongLineWarning = this._editor.getOption(EditorOption.hover).showLongLineWarning;
let stopRenderingMessage = false;
if (stopRenderingLineAfter >= 0 && lineLength > stopRenderingLineAfter && anchor.range.startColumn >= stopRenderingLineAfter) {
stopRenderingMessage = true;
result.push(new MarkdownHover(this, anchor.range, [{
value: nls.localize('stopped rendering', "Rendering paused for long line for performance reasons. This can be configured via `editor.stopRenderingLineAfter`.")
}], false, index++));
if (showLongLineWarning) {
result.push(new MarkdownHover(this, anchor.range, [{
value: nls.localize(
{ key: 'stopped rendering', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] },
"Rendering paused for long line for performance reasons. This can be configured via `editor.stopRenderingLineAfter`. [Don't Show Again](command:{0})",
HIDE_LONG_LINE_WARNING_HOVER_ACTION_ID
),
isTrusted: true
}], false, index++));
}
}
if (!stopRenderingMessage && typeof maxTokenizationLineLength === 'number' && lineLength >= maxTokenizationLineLength) {
result.push(new MarkdownHover(this, anchor.range, [{
value: nls.localize('too many characters', "Tokenization is skipped for long lines for performance reasons. This can be configured via `editor.maxTokenizationLineLength`.")
}], false, index++));
if (showLongLineWarning) {
result.push(new MarkdownHover(this, anchor.range, [{
value: nls.localize(
{ key: 'too many characters', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] },
"Tokenization is skipped for long lines for performance reasons. This can be configured via `editor.maxTokenizationLineLength`. [Don't Show Again](command:{0})",
HIDE_LONG_LINE_WARNING_HOVER_ACTION_ID
),
isTrusted: true
}], false, index++));
}
}

let isBeforeContent = false;
Expand Down
Loading
Loading