diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 0ccfb520c8a78..614f2ee7d1c74 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -330,6 +330,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d all = es.merge(all, gulp.src([ 'resources/win32/bower.ico', 'resources/win32/c.ico', + 'resources/win32/code.ico', 'resources/win32/config.ico', 'resources/win32/cpp.ico', 'resources/win32/csharp.ico', diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 5dee2dcd04f36..b20c54ebb013d 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -24,6 +24,7 @@ "--vscode-agentSessionReadIndicator-foreground", "--vscode-agentSessionSelectedBadge-border", "--vscode-agentSessionSelectedUnfocusedBadge-border", + "--vscode-agentStatusIndicator-background", "--vscode-badge-background", "--vscode-badge-foreground", "--vscode-banner-background", diff --git a/build/win32/code.iss b/build/win32/code.iss index 197ff3e9e2606..66d7e4d894331 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -288,7 +288,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.code-workspace\OpenW Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.code-workspace\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.code-workspace"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Code Workspace}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\code.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -649,7 +649,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ipynb\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ipynb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ipynb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jupyter}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\python.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1301,6 +1301,10 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValu Root: {#EnvironmentRootKey}; Subkey: "{#EnvironmentKey}"; ValueType: expandsz; ValueName: "Path"; ValueData: "{code:AddToPath|{app}\bin}"; Tasks: addtopath; Check: NeedsAddToPath(ExpandConstant('{app}\bin')) +; App Paths - allows running code from Explorer address bar +Root: {#EnvironmentRootKey}; Subkey: "Software\Microsoft\Windows\CurrentVersion\App Paths\{#ApplicationName}.exe"; ValueType: string; ValueName: ""; ValueData: "{app}\{#ExeBasename}.exe"; Flags: uninsdeletekey +Root: {#EnvironmentRootKey}; Subkey: "Software\Microsoft\Windows\CurrentVersion\App Paths\{#ApplicationName}.exe"; ValueType: string; ValueName: "Path"; ValueData: "{app}"; Flags: uninsdeletekey + [Code] function IsBackgroundUpdate(): Boolean; begin diff --git a/src/vs/base/node/osReleaseInfo.ts b/src/vs/base/node/osReleaseInfo.ts index 890bc254e16c0..d678dae83e433 100644 --- a/src/vs/base/node/osReleaseInfo.ts +++ b/src/vs/base/node/osReleaseInfo.ts @@ -66,6 +66,8 @@ export async function getOSReleaseInfo(errorLogger: (error: string | Error) => v return releaseInfo; } catch (err) { errorLogger(err); + } finally { + await handle.close(); } return; diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index c70fa95a387c9..f39515c3348a3 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -19,7 +19,7 @@ import { IWordAtPosition } from './core/wordHelper.js'; import { FormattingOptions } from './languages.js'; import { ILanguageSelection } from './languages/language.js'; import { IBracketPairsTextModelPart } from './textModelBracketPairs.js'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, InternalModelContentChangeEvent, ModelFontChangedEvent, ModelInjectedTextChangedEvent, ModelLineHeightChangedEvent } from './textModelEvents.js'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, ModelFontChangedEvent, ModelLineHeightChangedEvent } from './textModelEvents.js'; import { IModelContentChange } from './model/mirrorTextModel.js'; import { IGuidesTextModelPart } from './textModelGuides.js'; import { ITokenizationTextModelPart } from './tokenizationTextModelPart.js'; @@ -28,6 +28,7 @@ import { TokenArray } from './tokens/lineTokens.js'; import { IEditorModel } from './editorCommon.js'; import { TextModelEditSource } from './textModelEditSource.js'; import { TextEdit } from './core/edits/textEdit.js'; +import { IViewModel } from './viewModel.js'; /** * Vertical Lane in the overview ruler of the editor. @@ -715,6 +716,18 @@ export interface ITextModel { */ readonly isForSimpleWidget: boolean; + /** + * Method to register a view model on a model + * @internal + */ + registerViewModel(viewModel: IViewModel): void; + + /** + * Method which unregister a view model on a model + * @internal + */ + unregisterViewModel(viewModel: IViewModel): void; + /** * If true, the text model might contain RTL. * If false, the text model **contains only** contain LTR. @@ -1285,13 +1298,6 @@ export interface ITextModel { */ canRedo(): boolean; - /** - * @deprecated Please use `onDidChangeContent` instead. - * An event emitted when the contents of the model have changed. - * @internal - * @event - */ - readonly onDidChangeContentOrInjectedText: Event; /** * An event emitted when the contents of the model have changed. * @event diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index f38bd4218d345..ede464c5a82c4 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -11,7 +11,7 @@ import { Color } from '../../../base/common/color.js'; import { BugIndicatingError, illegalArgument, onUnexpectedError } from '../../../base/common/errors.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; -import { Disposable, IDisposable, MutableDisposable, combinedDisposable } from '../../../base/common/lifecycle.js'; +import { Disposable, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js'; import { listenStream } from '../../../base/common/stream.js'; import * as strings from '../../../base/common/strings.js'; import { ThemeColor } from '../../../base/common/themables.js'; @@ -54,6 +54,7 @@ import { AttachedViews } from './tokens/abstractSyntaxTokenBackend.js'; import { TokenizationFontDecorationProvider } from './tokens/tokenizationFontDecorationsProvider.js'; import { LineFontChangingDecoration, LineHeightChangingDecoration } from './decorationProvider.js'; import { TokenizationTextModelPart } from './tokens/tokenizationTextModelPart.js'; +import { IViewModel } from '../viewModel.js'; export function createTextBufferFactory(text: string): model.ITextBufferFactory { const builder = new PieceTreeTextBufferBuilder(); @@ -234,8 +235,6 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private readonly _onDidChangeAttached: Emitter = this._register(new Emitter()); public get onDidChangeAttached(): Event { return this._onDidChangeAttached.event; } - private readonly _onDidChangeInjectedText: Emitter = this._register(new Emitter()); - private readonly _onDidChangeLineHeight: Emitter = this._register(new Emitter()); public get onDidChangeLineHeight(): Event { return this._onDidChangeLineHeight.event; } @@ -244,13 +243,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private readonly _eventEmitter: DidChangeContentEmitter = this._register(new DidChangeContentEmitter()); public onDidChangeContent(listener: (e: IModelContentChangedEvent) => void): IDisposable { - return this._eventEmitter.slowEvent((e: InternalModelContentChangeEvent) => listener(e.contentChangedEvent)); - } - public onDidChangeContentOrInjectedText(listener: (e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent) => void): IDisposable { - return combinedDisposable( - this._eventEmitter.fastEvent(e => listener(e)), - this._onDidChangeInjectedText.event(e => listener(e)) - ); + return this._eventEmitter.event((e: InternalModelContentChangeEvent) => listener(e.contentChangedEvent)); } //#endregion @@ -307,6 +300,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati public get guides(): IGuidesTextModelPart { return this._guidesTextModelPart; } private readonly _attachedViews = new AttachedViews(); + private readonly _viewModels = new Set(); constructor( source: string | model.ITextBufferFactory, @@ -440,7 +434,6 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati || this._tokenizationTextModelPart._hasListeners() || this._onDidChangeOptions.hasListeners() || this._onDidChangeAttached.hasListeners() - || this._onDidChangeInjectedText.hasListeners() || this._onDidChangeLineHeight.hasListeners() || this._onDidChangeFont.hasListeners() || this._eventEmitter.hasListeners() @@ -453,6 +446,14 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } } + public registerViewModel(viewModel: IViewModel): void { + this._viewModels.add(viewModel); + } + + public unregisterViewModel(viewModel: IViewModel): void { + this._viewModels.delete(viewModel); + } + public equalsTextBuffer(other: model.ITextBuffer): boolean { this._assertNotDisposed(); return this._buffer.equals(other); @@ -463,7 +464,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return this._buffer; } - private _emitContentChangedEvent(rawChange: ModelRawContentChangedEvent, change: IModelContentChangedEvent): void { + private _emitContentChangedEvent(rawChange: ModelRawContentChangedEvent, change: IModelContentChangedEvent, resultingSelection: Selection[] | null = null): void { if (this.__isDisposing) { // Do not confuse listeners by emitting any event after disposing return; @@ -471,7 +472,13 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._tokenizationTextModelPart.handleDidChangeContent(change); this._bracketPairs.handleDidChangeContent(change); this._fontTokenDecorationsProvider.handleDidChangeContent(change); - this._eventEmitter.fire(new InternalModelContentChangeEvent(rawChange, change)); + const contentChangeEvent = new InternalModelContentChangeEvent(rawChange, change); + // Set resultingSelection early so viewModels can use it for cursor positioning + if (resultingSelection) { + contentChangeEvent.rawContentChangedEvent.resultingSelection = resultingSelection; + } + this._onDidChangeContentOrInjectedText(contentChangeEvent); + this._eventEmitter.fire(contentChangeEvent); } public setValue(value: string | model.ITextSnapshot, reason = EditSources.setValue()): void { @@ -1461,7 +1468,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._eventEmitter.beginDeferredEmit(); this._isUndoing = isUndoing; this._isRedoing = isRedoing; - this.applyEdits(edits, false); + const operations = this._validateEditOperations(edits); + this._doApplyEdits(operations, false, EditSources.applyEdits(), resultingSelection); this.setEOL(eol); this._overwriteAlternativeVersionId(resultingAlternativeVersionId); } finally { @@ -1492,7 +1500,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } } - private _doApplyEdits(rawOperations: model.ValidAnnotatedEditOperation[], computeUndoEdits: boolean, reason: TextModelEditSource): void | model.IValidEditOperation[] { + private _doApplyEdits(rawOperations: model.ValidAnnotatedEditOperation[], computeUndoEdits: boolean, reason: TextModelEditSource, resultingSelection: Selection[] | null = null): void | model.IValidEditOperation[] { const oldLineCount = this._buffer.getLineCount(); const result = this._buffer.applyEdits(rawOperations, this._options.trimAutoWhitespace, computeUndoEdits); @@ -1612,7 +1620,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati isFlush: false, detailedReasons: [reason], detailedReasonsChangeLengths: [contentChanges.length], - } + }, + resultingSelection ); } @@ -1645,7 +1654,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (affectedInjectedTextLines && affectedInjectedTextLines.size > 0) { const affectedLines = Array.from(affectedInjectedTextLines); const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, this.getLineContent(lineNumber), this._getInjectedTextInLine(lineNumber))); - this._onDidChangeInjectedText.fire(new ModelInjectedTextChangedEvent(lineChangeEvents)); + this._onDidChangeContentOrInjectedText(new ModelInjectedTextChangedEvent(lineChangeEvents)); } this._fireOnDidChangeLineHeight(affectedLineHeights); this._fireOnDidChangeFont(affectedFontLines); @@ -1667,6 +1676,15 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } } + private _onDidChangeContentOrInjectedText(e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent): void { + for (const viewModel of this._viewModels) { + viewModel.onDidChangeContentOrInjectedText(e); + } + for (const viewModel of this._viewModels) { + viewModel.emitContentChangeEvent(e); + } + } + public changeDecorations(callback: (changeAccessor: model.IModelDecorationsChangeAccessor) => T, ownerId: number = 0): T | null { this._assertNotDisposed(); @@ -2655,13 +2673,8 @@ class DidChangeDecorationsEmitter extends Disposable { class DidChangeContentEmitter extends Disposable { - /** - * Both `fastEvent` and `slowEvent` work the same way and contain the same events, but first we invoke `fastEvent` and then `slowEvent`. - */ - private readonly _fastEmitter: Emitter = this._register(new Emitter()); - public readonly fastEvent: Event = this._fastEmitter.event; - private readonly _slowEmitter: Emitter = this._register(new Emitter()); - public readonly slowEvent: Event = this._slowEmitter.event; + private readonly _emitter: Emitter = this._register(new Emitter()); + public readonly event: Event = this._emitter.event; private _deferredCnt: number; private _deferredEvent: InternalModelContentChangeEvent | null; @@ -2673,10 +2686,7 @@ class DidChangeContentEmitter extends Disposable { } public hasListeners(): boolean { - return ( - this._fastEmitter.hasListeners() - || this._slowEmitter.hasListeners() - ); + return this._emitter.hasListeners(); } public beginDeferredEmit(): void { @@ -2690,8 +2700,7 @@ class DidChangeContentEmitter extends Disposable { this._deferredEvent.rawContentChangedEvent.resultingSelection = resultingSelection; const e = this._deferredEvent; this._deferredEvent = null; - this._fastEmitter.fire(e); - this._slowEmitter.fire(e); + this._emitter.fire(e); } } } @@ -2705,7 +2714,6 @@ class DidChangeContentEmitter extends Disposable { } return; } - this._fastEmitter.fire(e); - this._slowEmitter.fire(e); + this._emitter.fire(e); } } diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index 2f3091955d0d3..10130ea8dcb08 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -324,7 +324,7 @@ export class ViewLayout extends Disposable implements IViewLayout { if (maxLineWidth > layoutInfo.contentWidth + fontInfo.typicalHalfwidthCharacterWidth) { // This is a case where viewport wrapping is on, but the line extends above the viewport if (minimap.enabled && minimap.side === 'right') { - // We need to accomodate the scrollbar width + // We need to accommodate the scrollbar width return maxLineWidth + layoutInfo.verticalScrollbarWidth; } } diff --git a/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index 6b25f3fbe619d..39b6149e8aa97 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -16,6 +16,7 @@ import { INewScrollPosition, ScrollType } from './editorCommon.js'; import { EditorTheme } from './editorTheme.js'; import { EndOfLinePreference, IGlyphMarginLanesModel, IModelDecorationOptions, ITextModel, TextDirection } from './model.js'; import { ILineBreaksComputer, InjectedText } from './modelLineProjectionData.js'; +import { InternalModelContentChangeEvent, ModelInjectedTextChangedEvent } from './textModelEvents.js'; import { BracketGuideOptions, IActiveIndentGuideInfo, IndentGuide } from './textModelGuides.js'; import { IViewLineTokens } from './tokens/lineTokens.js'; import { ViewEventHandler } from './viewEventHandler.js'; @@ -85,6 +86,9 @@ export interface IViewModel extends ICursorSimpleModel, ISimpleModel { getPlainTextToCopy(modelRanges: Range[], emptySelectionClipboard: boolean, forceCRLF: boolean): { sourceRanges: Range[]; sourceText: string | string[] }; getRichTextToCopy(modelRanges: Range[], emptySelectionClipboard: boolean): { html: string; mode: string } | null; + onDidChangeContentOrInjectedText(e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent): void; + emitContentChangeEvent(e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent): void; + createLineBreaksComputer(): ILineBreaksComputer; //#region cursor diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 0657caff859a6..954072212bce0 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -166,6 +166,7 @@ export class ViewModel extends Disposable implements IViewModel { })); this._updateConfigurationViewLineCountNow(); + this.model.registerViewModel(this); } public override dispose(): void { @@ -176,6 +177,7 @@ export class ViewModel extends Disposable implements IViewModel { this._lines.dispose(); this._viewportStart.dispose(); this._eventDispatcher.dispose(); + this.model.unregisterViewModel(this); } public getEditorOption(id: T): FindComputedEditorOptionValueById { @@ -311,143 +313,149 @@ export class ViewModel extends Disposable implements IViewModel { } } - private _registerModelEvents(): void { + /** + * Gets called directly by the text model. + */ + onDidChangeContentOrInjectedText(e: textModelEvents.InternalModelContentChangeEvent | textModelEvents.ModelInjectedTextChangedEvent): void { - this._register(this.model.onDidChangeContentOrInjectedText((e) => { - try { - const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); + try { + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); - let hadOtherModelChange = false; - let hadModelLineChangeThatChangedLineMapping = false; - - const changes = (e instanceof textModelEvents.InternalModelContentChangeEvent ? e.rawContentChangedEvent.changes : e.changes); - const versionId = (e instanceof textModelEvents.InternalModelContentChangeEvent ? e.rawContentChangedEvent.versionId : null); - - // Do a first pass to compute line mappings, and a second pass to actually interpret them - const lineBreaksComputer = this._lines.createLineBreaksComputer(); - for (const change of changes) { - switch (change.changeType) { - case textModelEvents.RawContentChangedType.LinesInserted: { - for (let lineIdx = 0; lineIdx < change.detail.length; lineIdx++) { - const line = change.detail[lineIdx]; - let injectedText = change.injectedTexts[lineIdx]; - if (injectedText) { - injectedText = injectedText.filter(element => (!element.ownerId || element.ownerId === this._editorId)); - } - lineBreaksComputer.addRequest(line, injectedText, null); + let hadOtherModelChange = false; + let hadModelLineChangeThatChangedLineMapping = false; + + const changes = (e instanceof textModelEvents.InternalModelContentChangeEvent ? e.rawContentChangedEvent.changes : e.changes); + const versionId = (e instanceof textModelEvents.InternalModelContentChangeEvent ? e.rawContentChangedEvent.versionId : null); + + // Do a first pass to compute line mappings, and a second pass to actually interpret them + const lineBreaksComputer = this._lines.createLineBreaksComputer(); + for (const change of changes) { + switch (change.changeType) { + case textModelEvents.RawContentChangedType.LinesInserted: { + for (let lineIdx = 0; lineIdx < change.detail.length; lineIdx++) { + const line = change.detail[lineIdx]; + let injectedText = change.injectedTexts[lineIdx]; + if (injectedText) { + injectedText = injectedText.filter(element => (!element.ownerId || element.ownerId === this._editorId)); } - break; + lineBreaksComputer.addRequest(line, injectedText, null); } - case textModelEvents.RawContentChangedType.LineChanged: { - let injectedText: textModelEvents.LineInjectedText[] | null = null; - if (change.injectedText) { - injectedText = change.injectedText.filter(element => (!element.ownerId || element.ownerId === this._editorId)); - } - lineBreaksComputer.addRequest(change.detail, injectedText, null); - break; + break; + } + case textModelEvents.RawContentChangedType.LineChanged: { + let injectedText: textModelEvents.LineInjectedText[] | null = null; + if (change.injectedText) { + injectedText = change.injectedText.filter(element => (!element.ownerId || element.ownerId === this._editorId)); } + lineBreaksComputer.addRequest(change.detail, injectedText, null); + break; } } - const lineBreaks = lineBreaksComputer.finalize(); - const lineBreakQueue = new ArrayQueue(lineBreaks); - - for (const change of changes) { - switch (change.changeType) { - case textModelEvents.RawContentChangedType.Flush: { - this._lines.onModelFlushed(); - eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent()); - this._decorations.reset(); - this.viewLayout.onFlushed(this.getLineCount(), this._getCustomLineHeights()); - hadOtherModelChange = true; - break; + } + const lineBreaks = lineBreaksComputer.finalize(); + const lineBreakQueue = new ArrayQueue(lineBreaks); + + for (const change of changes) { + switch (change.changeType) { + case textModelEvents.RawContentChangedType.Flush: { + this._lines.onModelFlushed(); + eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent()); + this._decorations.reset(); + this.viewLayout.onFlushed(this.getLineCount(), this._getCustomLineHeights()); + hadOtherModelChange = true; + break; + } + case textModelEvents.RawContentChangedType.LinesDeleted: { + const linesDeletedEvent = this._lines.onModelLinesDeleted(versionId, change.fromLineNumber, change.toLineNumber); + if (linesDeletedEvent !== null) { + eventsCollector.emitViewEvent(linesDeletedEvent); + this.viewLayout.onLinesDeleted(linesDeletedEvent.fromLineNumber, linesDeletedEvent.toLineNumber); } - case textModelEvents.RawContentChangedType.LinesDeleted: { - const linesDeletedEvent = this._lines.onModelLinesDeleted(versionId, change.fromLineNumber, change.toLineNumber); - if (linesDeletedEvent !== null) { - eventsCollector.emitViewEvent(linesDeletedEvent); - this.viewLayout.onLinesDeleted(linesDeletedEvent.fromLineNumber, linesDeletedEvent.toLineNumber); - } - hadOtherModelChange = true; - break; + hadOtherModelChange = true; + break; + } + case textModelEvents.RawContentChangedType.LinesInserted: { + const insertedLineBreaks = lineBreakQueue.takeCount(change.detail.length); + const linesInsertedEvent = this._lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks); + if (linesInsertedEvent !== null) { + eventsCollector.emitViewEvent(linesInsertedEvent); + this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); } - case textModelEvents.RawContentChangedType.LinesInserted: { - const insertedLineBreaks = lineBreakQueue.takeCount(change.detail.length); - const linesInsertedEvent = this._lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks); - if (linesInsertedEvent !== null) { - eventsCollector.emitViewEvent(linesInsertedEvent); - this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); - } - hadOtherModelChange = true; - break; + hadOtherModelChange = true; + break; + } + case textModelEvents.RawContentChangedType.LineChanged: { + const changedLineBreakData = lineBreakQueue.dequeue()!; + const [lineMappingChanged, linesChangedEvent, linesInsertedEvent, linesDeletedEvent] = + this._lines.onModelLineChanged(versionId, change.lineNumber, changedLineBreakData); + hadModelLineChangeThatChangedLineMapping = lineMappingChanged; + if (linesChangedEvent) { + eventsCollector.emitViewEvent(linesChangedEvent); } - case textModelEvents.RawContentChangedType.LineChanged: { - const changedLineBreakData = lineBreakQueue.dequeue()!; - const [lineMappingChanged, linesChangedEvent, linesInsertedEvent, linesDeletedEvent] = - this._lines.onModelLineChanged(versionId, change.lineNumber, changedLineBreakData); - hadModelLineChangeThatChangedLineMapping = lineMappingChanged; - if (linesChangedEvent) { - eventsCollector.emitViewEvent(linesChangedEvent); - } - if (linesInsertedEvent) { - eventsCollector.emitViewEvent(linesInsertedEvent); - this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); - } - if (linesDeletedEvent) { - eventsCollector.emitViewEvent(linesDeletedEvent); - this.viewLayout.onLinesDeleted(linesDeletedEvent.fromLineNumber, linesDeletedEvent.toLineNumber); - } - break; + if (linesInsertedEvent) { + eventsCollector.emitViewEvent(linesInsertedEvent); + this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); } - case textModelEvents.RawContentChangedType.EOLChanged: { - // Nothing to do. The new version will be accepted below - break; + if (linesDeletedEvent) { + eventsCollector.emitViewEvent(linesDeletedEvent); + this.viewLayout.onLinesDeleted(linesDeletedEvent.fromLineNumber, linesDeletedEvent.toLineNumber); } + break; + } + case textModelEvents.RawContentChangedType.EOLChanged: { + // Nothing to do. The new version will be accepted below + break; } } + } - if (versionId !== null) { - this._lines.acceptVersionId(versionId); - } - this.viewLayout.onHeightMaybeChanged(); + if (versionId !== null) { + this._lines.acceptVersionId(versionId); + } + this.viewLayout.onHeightMaybeChanged(); - if (!hadOtherModelChange && hadModelLineChangeThatChangedLineMapping) { - eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); - this._cursor.onLineMappingChanged(eventsCollector); - this._decorations.onLineMappingChanged(); - } - } finally { - this._eventDispatcher.endEmitViewEvents(); + if (!hadOtherModelChange && hadModelLineChangeThatChangedLineMapping) { + eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); + this._cursor.onLineMappingChanged(eventsCollector); + this._decorations.onLineMappingChanged(); } + } finally { + this._eventDispatcher.endEmitViewEvents(); + } - // Update the configuration and reset the centered view line - const viewportStartWasValid = this._viewportStart.isValid; - this._viewportStart.invalidate(); - this._configuration.setModelLineCount(this.model.getLineCount()); - this._updateConfigurationViewLineCountNow(); - - // Recover viewport - if (!this._hasFocus && this.model.getAttachedEditorCount() >= 2 && viewportStartWasValid) { - const modelRange = this.model._getTrackedRange(this._viewportStart.modelTrackedRange); - if (modelRange) { - const viewPosition = this.coordinatesConverter.convertModelPositionToViewPosition(modelRange.getStartPosition()); - const viewPositionTop = this.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber); - this.viewLayout.setScrollPosition({ scrollTop: viewPositionTop + this._viewportStart.startLineDelta }, ScrollType.Immediate); - } + // Update the configuration and reset the centered view line + const viewportStartWasValid = this._viewportStart.isValid; + this._viewportStart.invalidate(); + this._configuration.setModelLineCount(this.model.getLineCount()); + this._updateConfigurationViewLineCountNow(); + + // Recover viewport + if (!this._hasFocus && this.model.getAttachedEditorCount() >= 2 && viewportStartWasValid) { + const modelRange = this.model._getTrackedRange(this._viewportStart.modelTrackedRange); + if (modelRange) { + const viewPosition = this.coordinatesConverter.convertModelPositionToViewPosition(modelRange.getStartPosition()); + const viewPositionTop = this.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber); + this.viewLayout.setScrollPosition({ scrollTop: viewPositionTop + this._viewportStart.startLineDelta }, ScrollType.Immediate); } + } - try { - const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); - if (e instanceof textModelEvents.InternalModelContentChangeEvent) { - eventsCollector.emitOutgoingEvent(new ModelContentChangedEvent(e.contentChangedEvent)); - } - this._cursor.onModelContentChanged(eventsCollector, e); - } finally { - this._eventDispatcher.endEmitViewEvents(); + this._handleVisibleLinesChanged(); + } + + /** + * Gets called directly by the text model. + */ + emitContentChangeEvent(e: textModelEvents.InternalModelContentChangeEvent | textModelEvents.ModelInjectedTextChangedEvent): void { + this._emitViewEvent((eventsCollector) => { + if (e instanceof textModelEvents.InternalModelContentChangeEvent) { + eventsCollector.emitOutgoingEvent(new ModelContentChangedEvent(e.contentChangedEvent)); } + this._cursor.onModelContentChanged(eventsCollector, e); + }); + } - this._handleVisibleLinesChanged(); - })); + private _registerModelEvents(): void { const allowVariableLineHeights = this._configuration.options.get(EditorOption.allowVariableLineHeights); if (allowVariableLineHeights) { @@ -1246,15 +1254,19 @@ export class ViewModel extends Disposable implements IViewModel { private _withViewEventsCollector(callback: (eventsCollector: ViewModelEventsCollector) => T): T { return this._transactionalTarget.batchChanges(() => { - try { - const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); - return callback(eventsCollector); - } finally { - this._eventDispatcher.endEmitViewEvents(); - } + return this._emitViewEvent(callback); }); } + private _emitViewEvent(callback: (eventsCollector: ViewModelEventsCollector) => T): T { + try { + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); + return callback(eventsCollector); + } finally { + this._eventDispatcher.endEmitViewEvents(); + } + } + public batchEvents(callback: () => void): void { this._withViewEventsCollector(() => { callback(); }); } diff --git a/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts index 4767de7021d31..2b9655e91504d 100644 --- a/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts +++ b/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts @@ -11,6 +11,9 @@ import { EndOfLineSequence, PositionAffinity } from '../../../common/model.js'; import { ViewEventHandler } from '../../../common/viewEventHandler.js'; import { ViewEvent } from '../../../common/viewEvents.js'; import { testViewModel } from './testViewModel.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { createTextModel } from '../../common/testTextModel.js'; +import { createCodeEditorServices, instantiateTestCodeEditor } from '../testCodeEditor.js'; suite('ViewModel', () => { @@ -86,6 +89,33 @@ suite('ViewModel', () => { }); }); + test('view models react first to model changes', () => { + const initialText = [ + 'Hello', + 'world' + ]; + const disposables = new DisposableStore(); + + const model = disposables.add(createTextModel(initialText.join('\n'))); + const instantiationService = createCodeEditorServices(disposables); + const ed1 = disposables.add(instantiateTestCodeEditor(instantiationService, model)); + disposables.add(instantiateTestCodeEditor(instantiationService, model)); + + // Add a nasty listener which modifies the model during the model change event + let isFirst = true; + disposables.add(ed1.onDidChangeModelContent((e) => { + if (isFirst) { + isFirst = false; + // delete the \n + model.applyEdits([{ range: new Range(1, 6, 2, 1), text: '' }]); + } + })); + + model.applyEdits([{ range: new Range(2, 6, 2, 6), text: '!' }]); + + disposables.dispose(); + }); + test('issue #44805: No visible lines via API call', () => { const text = [ 'line1', diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index e6544a65608fa..9113fbdff2d84 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -15,8 +15,10 @@ import { ILanguageService } from '../../../common/languages/language.js'; import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js'; import { NullState } from '../../../common/languages/nullTokenize.js'; import { TextModel } from '../../../common/model/textModel.js'; -import { InternalModelContentChangeEvent, ModelRawContentChangedEvent, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from '../../../common/textModelEvents.js'; +import { InternalModelContentChangeEvent, ModelInjectedTextChangedEvent, ModelRawContentChangedEvent, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from '../../../common/textModelEvents.js'; import { createModelServices, createTextModel, instantiateTextModel } from '../testTextModel.js'; +import { mock } from '../../../../base/test/common/mock.js'; +import { IViewModel } from '../../../common/viewModel.js'; // --------- utils @@ -98,23 +100,34 @@ suite('Editor Model - Model', () => { // --------- insert text eventing + function withEventCapturing(callback: () => void): ModelRawContentChangedEvent | null { + let e: ModelRawContentChangedEvent | null = null; + const spyViewModel = new class extends mock() { + override onDidChangeContentOrInjectedText(_e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent) { + if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) { + assert.fail('Unexpected assertion error'); + } + e = _e.rawContentChangedEvent; + } + override emitContentChangeEvent(e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent): void { } + }; + thisModel.registerViewModel(spyViewModel); + callback(); + thisModel.unregisterViewModel(spyViewModel); + return e; + } + test('model insert empty text does not trigger eventing', () => { - const disposable = thisModel.onDidChangeContentOrInjectedText((e) => { - assert.ok(false, 'was not expecting event'); + const e = withEventCapturing(() => { + thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '')]); }); - thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '')]); - disposable.dispose(); + assert.deepStrictEqual(e, null, 'was not expecting event'); }); test('model insert text without newline eventing', () => { - let e: ModelRawContentChangedEvent | null = null; - const disposable = thisModel.onDidChangeContentOrInjectedText((_e) => { - if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) { - assert.fail('Unexpected assertion error'); - } - e = _e.rawContentChangedEvent; + const e = withEventCapturing(() => { + thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]); }); - thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'foo My First Line', null) @@ -123,18 +136,12 @@ suite('Editor Model - Model', () => { false, false )); - disposable.dispose(); }); test('model insert text with one newline eventing', () => { - let e: ModelRawContentChangedEvent | null = null; - const disposable = thisModel.onDidChangeContentOrInjectedText((_e) => { - if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) { - assert.fail('Unexpected assertion error'); - } - e = _e.rawContentChangedEvent; + const e = withEventCapturing(() => { + thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]); }); - thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'My new line', null), @@ -144,7 +151,6 @@ suite('Editor Model - Model', () => { false, false )); - disposable.dispose(); }); @@ -198,22 +204,16 @@ suite('Editor Model - Model', () => { // --------- delete text eventing test('model delete empty text does not trigger eventing', () => { - const disposable = thisModel.onDidChangeContentOrInjectedText((e) => { - assert.ok(false, 'was not expecting event'); + const e = withEventCapturing(() => { + thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 1))]); }); - thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 1))]); - disposable.dispose(); + assert.deepStrictEqual(e, null, 'was not expecting event'); }); test('model delete text from one line eventing', () => { - let e: ModelRawContentChangedEvent | null = null; - const disposable = thisModel.onDidChangeContentOrInjectedText((_e) => { - if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) { - assert.fail('Unexpected assertion error'); - } - e = _e.rawContentChangedEvent; + const e = withEventCapturing(() => { + thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]); }); - thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'y First Line', null), @@ -222,18 +222,12 @@ suite('Editor Model - Model', () => { false, false )); - disposable.dispose(); }); test('model delete all text from a line eventing', () => { - let e: ModelRawContentChangedEvent | null = null; - const disposable = thisModel.onDidChangeContentOrInjectedText((_e) => { - if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) { - assert.fail('Unexpected assertion error'); - } - e = _e.rawContentChangedEvent; + const e = withEventCapturing(() => { + thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]); }); - thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, '', null), @@ -242,18 +236,12 @@ suite('Editor Model - Model', () => { false, false )); - disposable.dispose(); }); test('model delete text from two lines eventing', () => { - let e: ModelRawContentChangedEvent | null = null; - const disposable = thisModel.onDidChangeContentOrInjectedText((_e) => { - if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) { - assert.fail('Unexpected assertion error'); - } - e = _e.rawContentChangedEvent; + const e = withEventCapturing(() => { + thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]); }); - thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'My Second Line', null), @@ -263,18 +251,12 @@ suite('Editor Model - Model', () => { false, false )); - disposable.dispose(); }); test('model delete text from many lines eventing', () => { - let e: ModelRawContentChangedEvent | null = null; - const disposable = thisModel.onDidChangeContentOrInjectedText((_e) => { - if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) { - assert.fail('Unexpected assertion error'); - } - e = _e.rawContentChangedEvent; + const e = withEventCapturing(() => { + thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]); }); - thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'My Third Line', null), @@ -284,7 +266,6 @@ suite('Editor Model - Model', () => { false, false )); - disposable.dispose(); }); // --------- getValueInRange @@ -319,14 +300,9 @@ suite('Editor Model - Model', () => { // --------- setValue test('setValue eventing', () => { - let e: ModelRawContentChangedEvent | null = null; - const disposable = thisModel.onDidChangeContentOrInjectedText((_e) => { - if (e !== null || !(_e instanceof InternalModelContentChangeEvent)) { - assert.fail('Unexpected assertion error'); - } - e = _e.rawContentChangedEvent; + const e = withEventCapturing(() => { + thisModel.setValue('new value'); }); - thisModel.setValue('new value'); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawFlush() @@ -335,7 +311,6 @@ suite('Editor Model - Model', () => { false, false )); - disposable.dispose(); }); test('issue #46342: Maintain edit operation order in applyEdits', () => { diff --git a/src/vs/editor/test/common/model/modelInjectedText.test.ts b/src/vs/editor/test/common/model/modelInjectedText.test.ts index b342e6bfb0135..d01509f642110 100644 --- a/src/vs/editor/test/common/model/modelInjectedText.test.ts +++ b/src/vs/editor/test/common/model/modelInjectedText.test.ts @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { mock } from '../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { EditOperation } from '../../../common/core/editOperation.js'; import { Range } from '../../../common/core/range.js'; -import { InternalModelContentChangeEvent, LineInjectedText, ModelRawChange, RawContentChangedType } from '../../../common/textModelEvents.js'; +import { InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, RawContentChangedType } from '../../../common/textModelEvents.js'; +import { IViewModel } from '../../../common/viewModel.js'; import { createTextModel } from '../testTextModel.js'; suite('Editor Model - Injected Text Events', () => { @@ -18,12 +20,16 @@ suite('Editor Model - Injected Text Events', () => { const recordedChanges = new Array(); - store.add(thisModel.onDidChangeContentOrInjectedText((e) => { - const changes = (e instanceof InternalModelContentChangeEvent ? e.rawContentChangedEvent.changes : e.changes); - for (const change of changes) { - recordedChanges.push(mapChange(change)); + const spyViewModel = new class extends mock() { + override onDidChangeContentOrInjectedText(e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent) { + const changes = (e instanceof InternalModelContentChangeEvent ? e.rawContentChangedEvent.changes : e.changes); + for (const change of changes) { + recordedChanges.push(mapChange(change)); + } } - })); + override emitContentChangeEvent(_e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent): void { } + }; + thisModel.registerViewModel(spyViewModel); // Initial decoration let decorations = thisModel.deltaDecorations([], [{ @@ -158,6 +164,8 @@ suite('Editor Model - Injected Text Events', () => { kind: 'linesDeleted', } ]); + + thisModel.unregisterViewModel(spyViewModel); }); }); diff --git a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts index 97a0519a493cb..96cdf66125cc6 100644 --- a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts +++ b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts @@ -120,8 +120,6 @@ export class ExtensionHostStarter extends Disposable implements IDisposable, IEx execArgv: opts.execArgv, allowLoadingUnsignedLibraries: true, respondToAuthRequestsFromMainProcess: true, - windowLifecycleBound: true, - windowLifecycleGraceTime: 6000, correlationId: id }); const pid = await Event.toPromise(extHost.onSpawn); diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index b375d171c42ac..008c4a4386662 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -618,7 +618,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS protected restartWatching(watcher: ParcelWatcherInstance, delay = 800): void { - // Restart watcher delayed to accomodate for + // Restart watcher delayed to accommodate for // changes on disk that have triggered the // need for a restart in the first place. const scheduler = new RunOnceScheduler(async () => { diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts index fccddba015651..05d83649c013e 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts @@ -99,14 +99,6 @@ export interface IWindowUtilityProcessConfiguration extends IUtilityProcessConfi * when the associated browser window closes or reloads. */ readonly windowLifecycleBound?: boolean; - - /** - * Optional period in milliseconds to allow for graceful shutdown - * before forcefully killing the process when the window lifecycle ends. - * If not set or 0, the process will be killed immediately. - * This is useful for extension hosts that need time to deactivate extensions. - */ - readonly windowLifecycleGraceTime?: number; } function isWindowUtilityProcessConfiguration(config: IUtilityProcessConfiguration): config is IWindowUtilityProcessConfiguration { @@ -496,17 +488,11 @@ export class WindowUtilityProcess extends UtilityProcess { private registerWindowListeners(window: BrowserWindow, configuration: IWindowUtilityProcessConfiguration): void { // If the lifecycle of the utility process is bound to the window, - // we terminate the process if the window closes or changes. - // If a grace period is configured, we wait for the process to exit - // before terminating (e.g. extensions need time to deactivate). + // we kill the process if the window closes or changes if (configuration.windowLifecycleBound) { - const graceTime = configuration.windowLifecycleGraceTime; - const terminate = graceTime && graceTime > 0 - ? () => this.waitForExit(graceTime) - : () => this.kill(); - this._register(Event.filter(this.lifecycleMainService.onWillLoadWindow, e => e.window.win === window)(terminate)); - this._register(Event.fromNodeEventEmitter(window, 'closed')(terminate)); + this._register(Event.filter(this.lifecycleMainService.onWillLoadWindow, e => e.window.win === window)(() => this.kill())); + this._register(Event.fromNodeEventEmitter(window, 'closed')(() => this.kill())); } } } diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index d16fc13213f83..ec14952a790ff 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -373,7 +373,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { if (this.preview) { const indexOfPreview = this.indexOf(this.preview); if (targetIndex > indexOfPreview) { - targetIndex--; // accomodate for the fact that the preview editor closes + targetIndex--; // accommodate for the fact that the preview editor closes } this.replaceEditor(this.preview, newEditor, targetIndex, !makeActive); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css index 686bad9d7ba4c..6103e2786e2b4 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css @@ -56,7 +56,7 @@ flex: 1; min-width: 0; -webkit-app-region: no-drag; - background-color: var(--vscode-commandCenter-background); + background-color: var(--vscode-agentStatusIndicator-background); border: 1px solid var(--vscode-commandCenter-border, transparent); } @@ -218,7 +218,7 @@ align-items: center; height: 22px; border-radius: 6px; - background-color: var(--vscode-commandCenter-background); + background-color: var(--vscode-agentStatusIndicator-background); border: 1px solid var(--vscode-commandCenter-border, transparent); flex-shrink: 0; -webkit-app-region: no-drag; diff --git a/src/vs/workbench/contrib/chat/common/widget/chatColors.ts b/src/vs/workbench/contrib/chat/common/widget/chatColors.ts index ad98943587cba..bb256ebba1988 100644 --- a/src/vs/workbench/contrib/chat/common/widget/chatColors.ts +++ b/src/vs/workbench/contrib/chat/common/widget/chatColors.ts @@ -7,6 +7,15 @@ import { Color, RGBA } from '../../../../../base/common/color.js'; import { localize } from '../../../../../nls.js'; import { badgeBackground, badgeForeground, contrastBorder, editorBackground, editorSelectionBackground, editorWidgetBackground, foreground, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js'; +// This color intentionally matches commandCenter.background but is separate so that it +// doesn't get overridden when debugging (the debug toolbar overrides commandCenter.background). +// This allows themes to customize it while maintaining independence from debug mode changes. +export const agentStatusIndicatorBackground = registerColor( + 'agentStatusIndicator.background', + { dark: Color.white.transparent(0.05), light: Color.black.transparent(0.05), hcDark: null, hcLight: null }, + localize('agentStatusIndicator.background', 'Background color of the agent status indicator in the titlebar.') +); + export const chatRequestBorder = registerColor( 'chat.requestBorder', { dark: new Color(new RGBA(255, 255, 255, 0.10)), light: new Color(new RGBA(0, 0, 0, 0.10)), hcDark: contrastBorder, hcLight: contrastBorder, }, diff --git a/src/vs/workbench/contrib/update/test/browser/updateStatusBarEntry.test.ts b/src/vs/workbench/contrib/update/test/browser/updateStatusBarEntry.test.ts index 16f10c93674c5..accef62179501 100644 --- a/src/vs/workbench/contrib/update/test/browser/updateStatusBarEntry.test.ts +++ b/src/vs/workbench/contrib/update/test/browser/updateStatusBarEntry.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import * as sinon from 'sinon'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { Downloading, StateType } from '../../../../../platform/update/common/update.js'; import { computeDownloadSpeed, computeDownloadTimeRemaining, formatBytes, formatTimeRemaining } from '../../browser/updateStatusBarEntry.js'; @@ -11,6 +12,16 @@ import { computeDownloadSpeed, computeDownloadTimeRemaining, formatBytes, format suite('UpdateStatusBarEntry', () => { ensureNoDisposablesAreLeakedInTestSuite(); + let clock: sinon.SinonFakeTimers; + + setup(() => { + clock = sinon.useFakeTimers(); + }); + + teardown(() => { + clock.restore(); + }); + function createDownloadingState(downloadedBytes?: number, totalBytes?: number, startTime?: number): Downloading { return { type: StateType.Downloading, explicit: true, overwrite: false, downloadedBytes, totalBytes, startTime }; } diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 5266ffe971735..e2e13e4ab1d39 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -6477,6 +6477,21 @@ declare module 'vscode' { */ export type CharacterPair = [string, string]; + /** + * Configuration for line comments. + */ + export interface LineCommentRule { + /** + * The line comment token, like `//` + */ + comment: string; + /** + * Whether the comment token should not be indented and placed at the first column. + * Defaults to false. + */ + noIndent?: boolean; + } + /** * Describes how comments for a language work. */ @@ -6485,7 +6500,7 @@ declare module 'vscode' { /** * The line comment token, like `// this is a comment` */ - lineComment?: string; + lineComment?: string | LineCommentRule; /** * The block comment character pair, like `/* block comment */` diff --git a/test/smoke/extensions/vscode-smoketest-ext-host/extension.js b/test/smoke/extensions/vscode-smoketest-ext-host/extension.js deleted file mode 100644 index e69899a6b29e5..0000000000000 --- a/test/smoke/extensions/vscode-smoketest-ext-host/extension.js +++ /dev/null @@ -1,90 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// @ts-check -const vscode = require('vscode'); -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -/** @type {string | undefined} */ -let deactivateMarkerFile; - -/** - * @param {vscode.ExtensionContext} context - */ -function activate(context) { - // This is used to verify that the extension host process is properly killed - // when window reloads even if the extension host is blocked - // Refs: https://github.com/microsoft/vscode/issues/291346 - context.subscriptions.push( - vscode.commands.registerCommand('smoketest.getExtensionHostPidAndBlock', (delayMs = 100, durationMs = 60000) => { - const pid = process.pid; - - // Write PID file to workspace folder if available, otherwise temp dir - // Note: filename must match name in extension-host-restart.test.ts - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - const pidFile = workspaceFolder - ? path.join(workspaceFolder, 'vscode-ext-host-pid.txt') - : path.join(os.tmpdir(), 'vscode-ext-host-pid.txt'); - setTimeout(() => { - fs.writeFileSync(pidFile, String(pid), 'utf-8'); - - // Block the extension host without busy-spinning to avoid pegging a CPU core. - // Prefer Atomics.wait on a SharedArrayBuffer when available; otherwise, fall back - // to the original busy loop to preserve behavior in older environments. - if (typeof SharedArrayBuffer === 'function' && typeof Atomics !== 'undefined' && typeof Atomics.wait === 'function') { - const sab = new SharedArrayBuffer(4); - const blocker = new Int32Array(sab); - // Wait up to durationMs milliseconds. This blocks the thread without consuming CPU. - Atomics.wait(blocker, 0, 0, durationMs); - } else { - const start = Date.now(); - while (Date.now() - start < durationMs) { - // Busy loop (fallback) - } - } - }, delayMs); - - return pid; - }) - ); - - // This command sets up a marker file path that will be written during deactivation. - // It allows the smoke test to verify that extensions get a chance to deactivate. - context.subscriptions.push( - vscode.commands.registerCommand('smoketest.setupGracefulDeactivation', () => { - const pid = process.pid; - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - const pidFile = workspaceFolder - ? path.join(workspaceFolder, 'vscode-ext-host-pid-graceful.txt') - : path.join(os.tmpdir(), 'vscode-ext-host-pid-graceful.txt'); - deactivateMarkerFile = workspaceFolder - ? path.join(workspaceFolder, 'vscode-ext-host-deactivated.txt') - : path.join(os.tmpdir(), 'vscode-ext-host-deactivated.txt'); - - // Write PID file immediately so test knows the extension is ready - fs.writeFileSync(pidFile, String(pid), 'utf-8'); - - return { pid, markerFile: deactivateMarkerFile }; - }) - ); -} - -function deactivate() { - // Write marker file to indicate deactivation was called - if (deactivateMarkerFile) { - try { - fs.writeFileSync(deactivateMarkerFile, `deactivated at ${Date.now()}`, 'utf-8'); - } catch { - // Ignore errors (e.g., folder not accessible) - } - } -} - -module.exports = { - activate, - deactivate -}; diff --git a/test/smoke/extensions/vscode-smoketest-ext-host/package.json b/test/smoke/extensions/vscode-smoketest-ext-host/package.json deleted file mode 100644 index 4b517b57d520e..0000000000000 --- a/test/smoke/extensions/vscode-smoketest-ext-host/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "vscode-smoketest-ext-host", - "displayName": "Smoke Test Extension Host", - "description": "Extension for smoke testing extension host lifecycle", - "version": "0.0.1", - "publisher": "vscode", - "license": "MIT", - "private": true, - "engines": { - "vscode": "^1.55.0" - }, - "activationEvents": [ - "onStartupFinished" - ], - "main": "./extension.js", - "extensionKind": ["ui"], - "contributes": { - "commands": [ - { - "command": "smoketest.getExtensionHostPidAndBlock", - "title": "Smoke Test: Get Extension Host PID and Block" - }, - { - "command": "smoketest.setupGracefulDeactivation", - "title": "Smoke Test: Setup Graceful Deactivation" - } - ] - } -} diff --git a/test/smoke/src/areas/extensions/extension-host-restart.test.ts b/test/smoke/src/areas/extensions/extension-host-restart.test.ts deleted file mode 100644 index 2fe6750aff50b..0000000000000 --- a/test/smoke/src/areas/extensions/extension-host-restart.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as fs from 'fs'; -import * as path from 'path'; -import { Application, Logger } from '../../../../automation'; -import { installAllHandlers, timeout } from '../../utils'; - -/** - * Verifies that window reload kills the extension host even when blocked. - * - */ -export function setup(logger: Logger) { - describe('Extension Host Restart', () => { - - installAllHandlers(logger, opts => opts); - - function processExists(pid: number): boolean { - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } - } - - it('kills blocked extension host on window reload (windowLifecycleBound)', async function () { - this.timeout(60_000); - - const app = this.app as Application; - const pidFile = path.join(app.workspacePathOrFolder, 'vscode-ext-host-pid.txt'); - - if (fs.existsSync(pidFile)) { - fs.unlinkSync(pidFile); - } - - await app.workbench.quickaccess.runCommand('smoketest.getExtensionHostPidAndBlock'); - - // Wait for PID file to be created - let retries = 0; - while (!fs.existsSync(pidFile) && retries < 20) { - await timeout(500); - retries++; - } - - if (!fs.existsSync(pidFile)) { - throw new Error('PID file was not created - extension may not have activated'); - } - - const pid = parseInt(fs.readFileSync(pidFile, 'utf-8'), 10); - logger.log(`Old extension host PID: ${pid}`); - - // Reload window while extension host is blocked - await app.workbench.quickaccess.runCommand('Developer: Reload Window'); - await app.code.whenWorkbenchRestored(); - logger.log('Window reloaded'); - - // Verify old process is gone, allowing for slower teardown on busy machines - const maxWaitMs = 10_000; - const pollIntervalMs = 500; - let waitedMs = 0; - while (processExists(pid) && waitedMs < maxWaitMs) { - await timeout(pollIntervalMs); - waitedMs += pollIntervalMs; - } - - const stillExists = processExists(pid); - if (stillExists) { - throw new Error(`Extension host ${pid} still running after reload (waited ${maxWaitMs}ms)`); - } - - logger.log('Extension host was properly killed on reload'); - }); - - it('allows extensions to gracefully deactivate on window reload (windowLifecycleGraceTime)', async function () { - this.timeout(60_000); - - const app = this.app as Application; - const pidFile = path.join(app.workspacePathOrFolder, 'vscode-ext-host-pid-graceful.txt'); - const markerFile = path.join(app.workspacePathOrFolder, 'vscode-ext-host-deactivated.txt'); - - // Clean up any existing files - if (fs.existsSync(pidFile)) { - fs.unlinkSync(pidFile); - } - if (fs.existsSync(markerFile)) { - fs.unlinkSync(markerFile); - } - - // Setup the extension to write a marker file on deactivation - await app.workbench.quickaccess.runCommand('smoketest.setupGracefulDeactivation'); - - // Wait for PID file to be created (confirms extension is ready) - let retries = 0; - while (!fs.existsSync(pidFile) && retries < 20) { - await timeout(500); - retries++; - } - - if (!fs.existsSync(pidFile)) { - throw new Error('PID file was not created - extension may not have activated'); - } - - const pid = parseInt(fs.readFileSync(pidFile, 'utf-8'), 10); - logger.log(`Extension host PID for graceful deactivation test: ${pid}`); - - // Reload window - this should trigger graceful deactivation - await app.workbench.quickaccess.runCommand('Developer: Reload Window'); - await app.code.whenWorkbenchRestored(); - logger.log('Window reloaded'); - - // Wait for the process to exit and marker file to be written - const maxWaitMs = 10_000; - const pollIntervalMs = 500; - let waitedMs = 0; - while (!fs.existsSync(markerFile) && waitedMs < maxWaitMs) { - await timeout(pollIntervalMs); - waitedMs += pollIntervalMs; - } - - if (!fs.existsSync(markerFile)) { - throw new Error(`Deactivation marker file was not created within ${maxWaitMs}ms - extension may not have been given time to deactivate gracefully`); - } - - logger.log('Extension was given time to gracefully deactivate on reload'); - - // Also verify the old process is gone - waitedMs = 0; - while (processExists(pid) && waitedMs < maxWaitMs) { - await timeout(pollIntervalMs); - waitedMs += pollIntervalMs; - } - - if (processExists(pid)) { - throw new Error(`Extension host ${pid} still running after reload (waited ${maxWaitMs}ms)`); - } - - logger.log('Extension host was properly terminated after graceful deactivation'); - }); - }); -} diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index c3e39215de62c..fc8b4f8800f96 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -21,7 +21,6 @@ import { setup as setupNotebookTests } from './areas/notebook/notebook.test'; import { setup as setupLanguagesTests } from './areas/languages/languages.test'; import { setup as setupStatusbarTests } from './areas/statusbar/statusbar.test'; import { setup as setupExtensionTests } from './areas/extensions/extensions.test'; -import { setup as setupExtensionHostRestartTests } from './areas/extensions/extension-host-restart.test'; import { setup as setupMultirootTests } from './areas/multiroot/multiroot.test'; import { setup as setupLocalizationTests } from './areas/workbench/localization.test'; import { setup as setupLaunchTests } from './areas/workbench/launch.test'; @@ -352,16 +351,6 @@ async function setup(): Promise { } await measureAndLog(() => setupRepository(), 'setupRepository', logger); - // Copy smoke test extension for extension host restart test - if (!opts.web) { - const smokeExtPath = path.join(rootPath, 'test', 'smoke', 'extensions', 'vscode-smoketest-ext-host'); - const dest = path.join(extensionsPath, 'vscode-smoketest-ext-host'); - if (fs.existsSync(dest)) { - fs.rmSync(dest, { recursive: true, force: true }); - } - fs.cpSync(smokeExtPath, dest, { recursive: true }); - } - logger.log('Smoketest setup done!\n'); } @@ -414,7 +403,6 @@ describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { setupTaskTests(logger); setupStatusbarTests(logger); if (quality !== Quality.Dev && quality !== Quality.OSS) { setupExtensionTests(logger); } - if (!opts.web) { setupExtensionHostRestartTests(logger); } setupMultirootTests(logger); if (!opts.web && !opts.remote && quality !== Quality.Dev && quality !== Quality.OSS) { setupLocalizationTests(logger); } if (!opts.web && !opts.remote) { setupLaunchTests(logger); }