From 4b9091414d9cad1d49006d81dbacce28ca64cf4e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 7 Dec 2021 05:52:32 -0800 Subject: Start of lockable hover widget Part of #115034 --- .../services/hover/browser/hoverService.ts | 28 +++++++++++++++++++--- .../services/hover/browser/hoverWidget.ts | 13 +++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) (limited to 'src/vs') diff --git a/src/vs/workbench/services/hover/browser/hoverService.ts b/src/vs/workbench/services/hover/browser/hoverService.ts index 4071dfb6a13..d42c56c9bff 100644 --- a/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/src/vs/workbench/services/hover/browser/hoverService.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/hover'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground, widgetShadow, textLinkActiveForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground, widgetShadow, textLinkActiveForeground, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { IHoverService, IHoverOptions, IHoverWidget } from 'vs/workbench/services/hover/browser/hover'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -19,6 +19,12 @@ export class HoverService implements IHoverService { declare readonly _serviceBrand: undefined; private _currentHoverOptions: IHoverOptions | undefined; + private _currentHover: HoverWidget | undefined; + + /** + * Whether the hover is "locked" by holding the control or command keys. When locked, the hover + * will not hide and can be hovered regardless of whether the `hideOnHover` hover option. + */ constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -52,7 +58,14 @@ export class HoverService implements IHoverService { } const focusedElement = document.activeElement; if (focusedElement) { - hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_DOWN, () => this.hideHover())); + hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_DOWN, e => { + // TODO: Cmd on mac + if (e.key === 'Control') { + hover.isLocked = true; + return; + } + this.hideHover(); + })); } if ('IntersectionObserver' in window) { @@ -62,13 +75,16 @@ export class HoverService implements IHoverService { hoverDisposables.add(toDisposable(() => observer.disconnect())); } + this._currentHover = hover; + return hover; } hideHover(): void { - if (!this._currentHoverOptions) { + if (this._currentHover?.isLocked || !this._currentHoverOptions) { return; } + this._currentHover = undefined; this._currentHoverOptions = undefined; this._contextViewService.hideContextView(); } @@ -124,6 +140,8 @@ registerThemingParticipant((theme, collector) => { const hoverBorder = theme.getColor(editorHoverBorder); if (hoverBorder) { collector.addRule(`.monaco-workbench .workbench-hover { border: 1px solid ${hoverBorder}; }`); + collector.addRule(`.monaco-workbench .workbench-hover-container.locked .workbench-hover { outline: 1px solid ${hoverBorder}; }`); + collector.addRule(`.monaco-workbench .workbench-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); collector.addRule(`.monaco-workbench .workbench-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); collector.addRule(`.monaco-workbench .workbench-hover hr { border-bottom: 0px solid ${hoverBorder.transparent(0.5)}; }`); @@ -131,6 +149,10 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-workbench .workbench-hover-pointer:after { border-right: 1px solid ${hoverBorder}; }`); collector.addRule(`.monaco-workbench .workbench-hover-pointer:after { border-bottom: 1px solid ${hoverBorder}; }`); } + const focus = theme.getColor(focusBorder); + if (focus) { + collector.addRule(`.monaco-workbench .workbench-hover-container.locked .workbench-hover:focus { outline-color: ${focus}; }`); + } const link = theme.getColor(textLinkForeground); if (link) { collector.addRule(`.monaco-workbench .workbench-hover a { color: ${link}; }`); diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index 78290d067fe..247f4148b44 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -51,6 +51,7 @@ export class HoverWidget extends Widget { private _forcePosition: boolean = false; private _x: number = 0; private _y: number = 0; + private _isLocked: boolean = false; get isDisposed(): boolean { return this._isDisposed; } get domNode(): HTMLElement { return this._hover.containerDomNode; } @@ -63,6 +64,12 @@ export class HoverWidget extends Widget { get anchor(): AnchorPosition { return this._hoverPosition === HoverPosition.BELOW ? AnchorPosition.BELOW : AnchorPosition.ABOVE; } get x(): number { return this._x; } get y(): number { return this._y; } + get isLocked(): boolean { return this._isLocked; } + set isLocked(value: boolean) { + this._isLocked = value; + this._hoverContainer.classList.toggle('locked', this._isLocked); + // TODO: Fire? + } constructor( options: IHoverOptions, @@ -183,7 +190,11 @@ export class HoverWidget extends Widget { mouseTrackerTargets.push(this._hoverContainer); } this._mouseTracker = new CompositeMouseTracker(mouseTrackerTargets); - this._register(this._mouseTracker.onMouseOut(() => this.dispose())); + this._register(this._mouseTracker.onMouseOut(() => { + if (!this._isLocked) { + this.dispose(); + } + })); this._register(this._mouseTracker); } -- cgit v1.2.3 From e2642fbfc6c9bdb521985b7e89b28377383529e0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 7 Dec 2021 06:06:10 -0800 Subject: Lock icon, fatter hover pointer --- src/vs/workbench/services/hover/browser/hoverWidget.ts | 15 ++++++++++++++- src/vs/workbench/services/hover/browser/media/hover.css | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) (limited to 'src/vs') diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index 247f4148b44..9ad21a4fc36 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import * as dom from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -18,6 +18,8 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { isMarkdownString } from 'vs/base/common/htmlContent'; +import { CancelablePromise, timeout } from 'vs/base/common/async'; +import { Codicon } from 'vs/base/common/codicons'; const $ = dom.$; type TargetRect = { @@ -66,8 +68,15 @@ export class HoverWidget extends Widget { get y(): number { return this._y; } get isLocked(): boolean { return this._isLocked; } set isLocked(value: boolean) { + if (this._isLocked) { + return; + } this._isLocked = value; this._hoverContainer.classList.toggle('locked', this._isLocked); + const lockElement = document.createElement('button'); + lockElement.classList.add('workbench-hover-lock'); + lockElement.classList.add(...Codicon.lockSmall.classNamesArray); + this._hoverContainer.append(lockElement); // TODO: Fire? } @@ -196,6 +205,10 @@ export class HoverWidget extends Widget { } })); this._register(this._mouseTracker); + + const autoLockTimeout: CancelablePromise | undefined = timeout(3000); + autoLockTimeout.then(() => this.isLocked = true); + this._register(toDisposable(() => autoLockTimeout?.cancel())); } public render(container: HTMLElement): void { diff --git a/src/vs/workbench/services/hover/browser/media/hover.css b/src/vs/workbench/services/hover/browser/media/hover.css index 215cb9b9fcc..86f02e61746 100644 --- a/src/vs/workbench/services/hover/browser/media/hover.css +++ b/src/vs/workbench/services/hover/browser/media/hover.css @@ -38,6 +38,12 @@ width: 5px; height: 5px; } +.monaco-workbench .locked .workbench-hover-pointer:after { + width: 4px; + height: 4px; + border-right-width: 2px; + border-bottom-width: 2px; +} .monaco-workbench .workbench-hover-pointer.left { left: -3px; } .monaco-workbench .workbench-hover-pointer.right { right: 3px; } @@ -85,3 +91,14 @@ margin-right: 0; margin-left: 16px; } + +.monaco-workbench .workbench-hover-lock { + position: absolute; + right: 1px; + top: 1px; + opacity: 0.5; + z-index: 100; + padding: 0; + border: 0; + background: none; +} -- cgit v1.2.3 From ccc3f63e46826cf60ea33becbc31b814c7479df1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 7 Dec 2021 06:22:23 -0800 Subject: Lock button click action --- .../workbench/services/hover/browser/hoverService.ts | 7 ++++++- src/vs/workbench/services/hover/browser/hoverWidget.ts | 18 ++++++++++++------ .../workbench/services/hover/browser/media/hover.css | 13 ++++++++++--- 3 files changed, 28 insertions(+), 10 deletions(-) (limited to 'src/vs') diff --git a/src/vs/workbench/services/hover/browser/hoverService.ts b/src/vs/workbench/services/hover/browser/hoverService.ts index d42c56c9bff..e124e4681c4 100644 --- a/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/src/vs/workbench/services/hover/browser/hoverService.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/hover'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground, widgetShadow, textLinkActiveForeground, focusBorder } from 'vs/platform/theme/common/colorRegistry'; +import { editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground, widgetShadow, textLinkActiveForeground, focusBorder, toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { IHoverService, IHoverOptions, IHoverWidget } from 'vs/workbench/services/hover/browser/hover'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -152,6 +152,11 @@ registerThemingParticipant((theme, collector) => { const focus = theme.getColor(focusBorder); if (focus) { collector.addRule(`.monaco-workbench .workbench-hover-container.locked .workbench-hover:focus { outline-color: ${focus}; }`); + collector.addRule(`.monaco-workbench .workbench-hover-lock:focus { outline: 1px solid ${focus}; }`); + } + const toolbarHoverBackgroundColor = theme.getColor(toolbarHoverBackground); + if (toolbarHoverBackgroundColor) { + collector.addRule(`.monaco-workbench .workbench-hover-container.locked .workbench-hover-lock:hover { background-color: ${toolbarHoverBackgroundColor}; }`); } const link = theme.getColor(textLinkForeground); if (link) { diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index 9ad21a4fc36..d5e476cc157 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -44,6 +44,7 @@ export class HoverWidget extends Widget { private readonly _hover: BaseHoverWidget; private readonly _hoverPointer: HTMLElement | undefined; + private _lockElement: HTMLElement | undefined; private readonly _hoverContainer: HTMLElement; private readonly _target: IHoverTarget; private readonly _linkHandler: (url: string) => any; @@ -68,16 +69,21 @@ export class HoverWidget extends Widget { get y(): number { return this._y; } get isLocked(): boolean { return this._isLocked; } set isLocked(value: boolean) { - if (this._isLocked) { + if (this._isLocked === value) { return; } this._isLocked = value; this._hoverContainer.classList.toggle('locked', this._isLocked); - const lockElement = document.createElement('button'); - lockElement.classList.add('workbench-hover-lock'); - lockElement.classList.add(...Codicon.lockSmall.classNamesArray); - this._hoverContainer.append(lockElement); - // TODO: Fire? + if (value) { + this._lockElement = document.createElement('button'); + this._lockElement.classList.add('workbench-hover-lock'); + this._lockElement.classList.add(...Codicon.lockSmall.classNamesArray); + this._lockElement.addEventListener('click', () => this.isLocked = false); + this._hoverContainer.append(this._lockElement); + } else { + this._lockElement?.remove(); + this._lockElement = undefined; + } } constructor( diff --git a/src/vs/workbench/services/hover/browser/media/hover.css b/src/vs/workbench/services/hover/browser/media/hover.css index 86f02e61746..e0eca663aaa 100644 --- a/src/vs/workbench/services/hover/browser/media/hover.css +++ b/src/vs/workbench/services/hover/browser/media/hover.css @@ -94,11 +94,18 @@ .monaco-workbench .workbench-hover-lock { position: absolute; - right: 1px; - top: 1px; - opacity: 0.5; + right: 2px; + top: 2px; z-index: 100; padding: 0; border: 0; background: none; } +.monaco-workbench .workbench-hover-lock::before { + opacity: 0.5; +} +.monaco-workbench .workbench-hover-lock:hover { + /* background-color: red; */ + border-radius: 5px; + cursor: pointer; +} -- cgit v1.2.3 From 2fc5889511c7374f8f53ee87f15979c681305919 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 7 Dec 2021 06:37:46 -0800 Subject: Don't auto lock after unlocking --- src/vs/workbench/services/hover/browser/hoverWidget.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'src/vs') diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index d5e476cc157..a628ccdb78d 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -44,11 +44,13 @@ export class HoverWidget extends Widget { private readonly _hover: BaseHoverWidget; private readonly _hoverPointer: HTMLElement | undefined; - private _lockElement: HTMLElement | undefined; private readonly _hoverContainer: HTMLElement; private readonly _target: IHoverTarget; private readonly _linkHandler: (url: string) => any; + private _lockElement: HTMLElement | undefined; + private _autoLockTimeout: CancelablePromise | undefined; + private _isDisposed: boolean = false; private _hoverPosition: HoverPosition; private _forcePosition: boolean = false; @@ -72,6 +74,8 @@ export class HoverWidget extends Widget { if (this._isLocked === value) { return; } + this._autoLockTimeout?.cancel(); + this._autoLockTimeout = undefined; this._isLocked = value; this._hoverContainer.classList.toggle('locked', this._isLocked); if (value) { @@ -212,9 +216,9 @@ export class HoverWidget extends Widget { })); this._register(this._mouseTracker); - const autoLockTimeout: CancelablePromise | undefined = timeout(3000); - autoLockTimeout.then(() => this.isLocked = true); - this._register(toDisposable(() => autoLockTimeout?.cancel())); + this._autoLockTimeout = timeout(3000); + this._autoLockTimeout.then(() => this.isLocked = true); + this._register(toDisposable(() => this._autoLockTimeout?.cancel())); } public render(container: HTMLElement): void { -- cgit v1.2.3 From 10b979fb39601eff4bf175d751ba06e4f02e7649 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Dec 2021 11:19:45 -0800 Subject: Increase auto lock timeout --- src/vs/workbench/services/hover/browser/hoverWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/vs') diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index f2bf97ebb7c..41ad2c77b51 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -216,7 +216,7 @@ export class HoverWidget extends Widget { })); this._register(this._mouseTracker); - this._autoLockTimeout = timeout(3000); + this._autoLockTimeout = timeout(5000); this._autoLockTimeout.then(() => this.isLocked = true); this._register(toDisposable(() => this._autoLockTimeout?.cancel())); } -- cgit v1.2.3 From 083e6b0e878e46494fe80fc77bc879a2b588cbc4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Dec 2021 11:42:44 -0800 Subject: Use alt instead of ctrl/cmd and unlock on release --- src/vs/workbench/services/hover/browser/hoverService.ts | 14 +++++++------- src/vs/workbench/services/hover/browser/hoverWidget.ts | 5 +++++ 2 files changed, 12 insertions(+), 7 deletions(-) (limited to 'src/vs') diff --git a/src/vs/workbench/services/hover/browser/hoverService.ts b/src/vs/workbench/services/hover/browser/hoverService.ts index e124e4681c4..8761101f39f 100644 --- a/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/src/vs/workbench/services/hover/browser/hoverService.ts @@ -21,11 +21,6 @@ export class HoverService implements IHoverService { private _currentHoverOptions: IHoverOptions | undefined; private _currentHover: HoverWidget | undefined; - /** - * Whether the hover is "locked" by holding the control or command keys. When locked, the hover - * will not hide and can be hovered regardless of whether the `hideOnHover` hover option. - */ - constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @IContextViewService private readonly _contextViewService: IContextViewService, @@ -59,13 +54,18 @@ export class HoverService implements IHoverService { const focusedElement = document.activeElement; if (focusedElement) { hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_DOWN, e => { - // TODO: Cmd on mac - if (e.key === 'Control') { + if (e.key === 'Alt') { hover.isLocked = true; return; } this.hideHover(); })); + hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_UP, e => { + if (e.key === 'Alt') { + hover.isLocked = false; + return; + } + })); } if ('IntersectionObserver' in window) { diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index 41ad2c77b51..e60a603f7dd 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -69,6 +69,11 @@ export class HoverWidget extends Widget { get anchor(): AnchorPosition { return this._hoverPosition === HoverPosition.BELOW ? AnchorPosition.BELOW : AnchorPosition.ABOVE; } get x(): number { return this._x; } get y(): number { return this._y; } + + /** + * Whether the hover is "locked" by holding the alt/option key. When locked, the hover will not + * hide and can be hovered regardless of whether the `hideOnHover` hover option. + */ get isLocked(): boolean { return this._isLocked; } set isLocked(value: boolean) { if (this._isLocked === value) { -- cgit v1.2.3 From 05f740c9ae4236b494c2df90728376fa84dddacb Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Dec 2021 11:45:32 -0800 Subject: Remove lock icon and auto lock timeout --- .../services/hover/browser/hoverWidget.ts | 23 +--------------------- .../services/hover/browser/media/hover.css | 18 ----------------- 2 files changed, 1 insertion(+), 40 deletions(-) (limited to 'src/vs') diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index e60a603f7dd..607eec25a63 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import * as dom from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -18,8 +18,6 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { isMarkdownString } from 'vs/base/common/htmlContent'; -import { CancelablePromise, timeout } from 'vs/base/common/async'; -import { Codicon } from 'vs/base/common/codicons'; const $ = dom.$; type TargetRect = { @@ -48,9 +46,6 @@ export class HoverWidget extends Widget { private readonly _target: IHoverTarget; private readonly _linkHandler: (url: string) => any; - private _lockElement: HTMLElement | undefined; - private _autoLockTimeout: CancelablePromise | undefined; - private _isDisposed: boolean = false; private _hoverPosition: HoverPosition; private _forcePosition: boolean = false; @@ -79,20 +74,8 @@ export class HoverWidget extends Widget { if (this._isLocked === value) { return; } - this._autoLockTimeout?.cancel(); - this._autoLockTimeout = undefined; this._isLocked = value; this._hoverContainer.classList.toggle('locked', this._isLocked); - if (value) { - this._lockElement = document.createElement('button'); - this._lockElement.classList.add('workbench-hover-lock'); - this._lockElement.classList.add(...Codicon.lockSmall.classNamesArray); - this._lockElement.addEventListener('click', () => this.isLocked = false); - this._hoverContainer.append(this._lockElement); - } else { - this._lockElement?.remove(); - this._lockElement = undefined; - } } constructor( @@ -220,10 +203,6 @@ export class HoverWidget extends Widget { } })); this._register(this._mouseTracker); - - this._autoLockTimeout = timeout(5000); - this._autoLockTimeout.then(() => this.isLocked = true); - this._register(toDisposable(() => this._autoLockTimeout?.cancel())); } public render(container: HTMLElement): void { diff --git a/src/vs/workbench/services/hover/browser/media/hover.css b/src/vs/workbench/services/hover/browser/media/hover.css index e0eca663aaa..d560a4a3a96 100644 --- a/src/vs/workbench/services/hover/browser/media/hover.css +++ b/src/vs/workbench/services/hover/browser/media/hover.css @@ -91,21 +91,3 @@ margin-right: 0; margin-left: 16px; } - -.monaco-workbench .workbench-hover-lock { - position: absolute; - right: 2px; - top: 2px; - z-index: 100; - padding: 0; - border: 0; - background: none; -} -.monaco-workbench .workbench-hover-lock::before { - opacity: 0.5; -} -.monaco-workbench .workbench-hover-lock:hover { - /* background-color: red; */ - border-radius: 5px; - cursor: pointer; -} -- cgit v1.2.3 From a51801b4654c5b38472d6142a494a56fb6640225 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 1 Apr 2022 11:02:02 +0530 Subject: align extension scanning - Extract extension scanning, validating and nls replacement into `INativeExtensionsScannerService` - Use `INativeExtensionsScannerService` for scanning in Desktop and Remote extension managements and extension hosts - Represent invalid extensions in Extensions UI - Remove prompting for invalid extensions while scanning in Desktop Extension Host in Dev mode --- .../sharedProcess/sharedProcessMain.ts | 2 + .../common/extensionManagementUtil.ts | 6 +- .../common/extensionsScannerService.ts | 676 +++++++++++++++++++ .../node/extensionManagementService.ts | 12 +- .../extensionManagement/node/extensionsScanner.ts | 291 ++------ .../extensions/common/extensionValidator.ts | 108 +++ src/vs/platform/extensions/common/extensions.ts | 3 + src/vs/server/node/remoteAgentEnvironmentImpl.ts | 194 ++---- src/vs/server/node/serverServices.ts | 5 +- .../extensions/browser/extensionsActions.ts | 5 + .../browser/builtinExtensionsScannerService.ts | 2 + .../browser/webExtensionsScannerService.ts | 21 +- .../common/extensionManagement.ts | 2 - .../extensions/common/abstractExtensionService.ts | 26 +- .../services/extensions/common/extensionPoints.ts | 750 --------------------- .../services/extensions/common/extensions.ts | 2 + .../services/extensions/common/extensionsUtil.ts | 10 +- .../electron-browser/extensionService.ts | 4 +- .../electron-sandbox/cachedExtensionScanner.ts | 230 +++---- src/vs/workbench/workbench.common.main.ts | 1 + 20 files changed, 1040 insertions(+), 1310 deletions(-) create mode 100644 src/vs/platform/extensionManagement/common/extensionsScannerService.ts delete mode 100644 src/vs/workbench/services/extensions/common/extensionPoints.ts (limited to 'src/vs') diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 0166a2b0e5d..c378b1c8bde 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -98,6 +98,7 @@ import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataPr import { DiskFileSystemProviderClient, LOCAL_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/files/common/diskFileSystemProviderClient'; import { InspectProfilingService as V8InspectProfilingService } from 'vs/platform/profiling/node/profilingService'; import { IV8InspectProfilingService } from 'vs/platform/profiling/common/profiling'; +import { NativeExtensionsScannerService, INativeExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; class SharedProcessMain extends Disposable { @@ -293,6 +294,7 @@ class SharedProcessMain extends Disposable { services.set(ICustomEndpointTelemetryService, customEndpointTelemetryService); // Extension Management + services.set(INativeExtensionsScannerService, new SyncDescriptor(NativeExtensionsScannerService)); services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); // Extension Gallery diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index 236b6e63174..5c6f4380295 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -27,9 +27,9 @@ const ExtensionKeyRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)(-(.+))?$/; export class ExtensionKey { - static create(extension: ILocalExtension | IGalleryExtension): ExtensionKey { - const version = (extension as ILocalExtension).manifest ? (extension as ILocalExtension).manifest.version : (extension as IGalleryExtension).version; - const targetPlatform = (extension as ILocalExtension).manifest ? (extension as ILocalExtension).targetPlatform : (extension as IGalleryExtension).properties.targetPlatform; + static create(extension: IExtension | IGalleryExtension): ExtensionKey { + const version = (extension as IExtension).manifest ? (extension as IExtension).manifest.version : (extension as IGalleryExtension).version; + const targetPlatform = (extension as IExtension).manifest ? (extension as IExtension).targetPlatform : (extension as IGalleryExtension).properties.targetPlatform; return new ExtensionKey(extension.identifier, version, targetPlatform); } diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts new file mode 100644 index 00000000000..f29b1271411 --- /dev/null +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -0,0 +1,676 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { coalesce } from 'vs/base/common/arrays'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { getErrorMessage } from 'vs/base/common/errors'; +import { getNodeType, parse, ParseError } from 'vs/base/common/json'; +import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { FileAccess, Schemas } from 'vs/base/common/network'; +import * as path from 'vs/base/common/path'; +import { basename, isEqualOrParent, joinPath } from 'vs/base/common/resources'; +import * as semver from 'vs/base/common/semver/semver'; +import Severity from 'vs/base/common/severity'; +import { isArray, isObject, isString } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; +import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; + +type IScannedExtensionManifest = IRelaxedExtensionManifest & { __metadata?: Metadata }; + +interface IRelaxedScannedExtension { + type: ExtensionType; + isBuiltin: boolean; + identifier: IExtensionIdentifier; + manifest: IRelaxedExtensionManifest; + location: URI; + targetPlatform: TargetPlatform; + metadata: Metadata | undefined; + isValid: boolean; + validations: readonly { readonly severity: Severity; readonly message: string }[]; +} + +export type IScannedExtension = Readonly & { manifest: IExtensionManifest }; + +export interface Translations { + [id: string]: string; +} + +export namespace Translations { + export function equals(a: Translations, b: Translations): boolean { + if (a === b) { + return true; + } + let aKeys = Object.keys(a); + let bKeys: Set = new Set(); + for (let key of Object.keys(b)) { + bKeys.add(key); + } + if (aKeys.length !== bKeys.size) { + return false; + } + + for (let key of aKeys) { + if (a[key] !== b[key]) { + return false; + } + bKeys.delete(key); + } + return bKeys.size === 0; + } +} + +export interface NlsConfiguration { + readonly devMode: boolean; + readonly locale: string | undefined; + readonly pseudo: boolean; + readonly translations: Translations; +} + +interface MessageBag { + [key: string]: string | { message: string; comment: string[] }; +} + +interface TranslationBundle { + contents: { + package: MessageBag; + }; +} + +interface LocalizedMessages { + values: MessageBag | undefined; + default: URI | null; +} + +interface IBuiltInExtensionControl { + [name: string]: 'marketplace' | 'disabled' | string; +} + +export type ScanOptions = { + readonly includeInvalid?: boolean; + readonly nlsConfiguration?: NlsConfiguration; + readonly includeAllVersions?: boolean; + readonly includeUninstalled?: boolean; + readonly checkControlFile?: boolean; +}; + +export const INativeExtensionsScannerService = createDecorator('INativeExtensionsScannerService'); +export interface INativeExtensionsScannerService { + readonly _serviceBrand: undefined; + + readonly systemExtensionsLocation: URI; + readonly userExtensionsLocation: URI; + + getTargetPlatform(): Promise; + + scanAllExtensions(scanOptions: ScanOptions): Promise; + scanSystemExtensions(scanOptions: ScanOptions): Promise; + scanUserExtensions(scanOptions: ScanOptions): Promise; + scanExtensionsUnderDevelopment(scanOptions: ScanOptions): Promise; + scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise; + scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise; + + updateMetadata(extensionLocation: URI, metadata: Partial): Promise; +} + +export class NativeExtensionsScannerService extends Disposable implements INativeExtensionsScannerService { + + readonly _serviceBrand: undefined; + + readonly systemExtensionsLocation: URI; + readonly userExtensionsLocation: URI; + + constructor( + @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService, + @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, + @IProductService private readonly productService: IProductService, + ) { + super(); + this.systemExtensionsLocation = URI.file(environmentService.builtinExtensionsPath); + this.userExtensionsLocation = URI.file(environmentService.extensionsPath); + } + + private _targetPlatformPromise: Promise | undefined; + getTargetPlatform(): Promise { + if (!this._targetPlatformPromise) { + this._targetPlatformPromise = computeTargetPlatform(this.fileService, this.logService); + } + return this._targetPlatformPromise; + } + + async scanAllExtensions(scanOptions: ScanOptions): Promise { + const [system, user, development] = await Promise.all([ + this.scanSystemExtensions(scanOptions), + this.scanUserExtensions(scanOptions), + this.scanExtensionsUnderDevelopment(scanOptions), + ]); + return this.dedupExtensions([...system, ...user, ...development], await this.getTargetPlatform(), true); + } + + async scanSystemExtensions(scanOptions: ScanOptions): Promise { + const promises: Promise[] = []; + promises.push(this.scanDefaultSystemExtensions()); + promises.push(this.scanDevSystemExtensions(scanOptions)); + try { + const [defaultSystemExtensions, devSystemExtensions] = await Promise.all(promises); + return this.applyScanOptions([...defaultSystemExtensions, ...devSystemExtensions], scanOptions, false); + } catch (error) { + throw this.joinErrors(error); + } + } + + async scanUserExtensions(scanOptions: ScanOptions): Promise { + this.logService.trace('Started scanning user extensions'); + let extensions: IRelaxedScannedExtension[]; + if (scanOptions.includeUninstalled) { + extensions = await this.scanFromUserExtensionsLocation(); + } else { + let [uninstalled, scannedExtensions] = await Promise.all([this.getUninstalledExtensions(), this.scanFromUserExtensionsLocation()]); + extensions = scannedExtensions.filter(e => !uninstalled[ExtensionKey.create(e).toString()]); + } + extensions = await this.applyScanOptions(extensions, scanOptions, true); + this.logService.trace('Scanned user extensions:', extensions.length); + return extensions; + } + + async scanExtensionsUnderDevelopment(scanOptions: ScanOptions): Promise { + if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) { + const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file) + .map(async extensionDevelopmentLocationURI => await this.doScanOneOrMultipleExtensions(extensionDevelopmentLocationURI, ExtensionType.User)))) + .flat(); + return this.applyScanOptions(extensions, scanOptions, true); + } + return []; + } + + async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { + const extension = await this.scanExtension(extensionLocation, extensionType); + if (!extension) { + return null; + } + if (!scanOptions.includeInvalid && !extension.isValid) { + return null; + } + extension.manifest = await this.localizeManifest(extension.location, extension.manifest, scanOptions.nlsConfiguration); + return extension; + } + + async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { + const extensions = await this.doScanOneOrMultipleExtensions(extensionLocation, extensionType); + return this.applyScanOptions(extensions, scanOptions, true); + } + + async updateMetadata(extensionLocation: URI, metaData: Partial): Promise { + const manifest = await this.scanExtensionManifest(extensionLocation); + if (!manifest) { + throw new Error(localize('cannot read', "Cannot read the extension manifest from {0}", extensionLocation.path)); + } + // unset if false + metaData.isMachineScoped = metaData.isMachineScoped || undefined; + metaData.isBuiltin = metaData.isBuiltin || undefined; + metaData.installedTimestamp = metaData.installedTimestamp || undefined; + manifest.__metadata = { ...manifest.__metadata, ...metaData }; + await this.fileService.writeFile(joinPath(extensionLocation, 'package.json'), VSBuffer.fromString(JSON.stringify(manifest, null, '\t'))); + } + + private async applyScanOptions(extensions: IRelaxedScannedExtension[], scanOptions: ScanOptions, pickLatest: boolean): Promise { + if (!scanOptions.includeAllVersions) { + extensions = this.dedupExtensions(extensions, await this.getTargetPlatform(), pickLatest); + } + if (!scanOptions.includeInvalid) { + extensions = extensions.filter(extension => extension.isValid); + } + extensions = await this.localizeExtensions(extensions, scanOptions.nlsConfiguration); + return extensions.sort((a, b) => { + const aLastSegment = path.basename(a.location.fsPath); + const bLastSegment = path.basename(b.location.fsPath); + if (aLastSegment < bLastSegment) { + return -1; + } + if (aLastSegment > bLastSegment) { + return 1; + } + return 0; + }); + } + + private async doScanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType): Promise { + try { + if (await this.fileService.exists(joinPath(extensionLocation, 'package.json'))) { + const extension = await this.scanExtension(extensionLocation, extensionType); + return extension ? [extension] : []; + } else { + return await this.scanExtensionsInLocation(extensionLocation, extensionType); + } + } catch (error) { + this.logService.error(`Error scanning extensions at ${extensionLocation.path}:`, getErrorMessage(error)); + return []; + } + } + + private async scanExtensionsInLocation(location: URI, preferredType: ExtensionType): Promise { + const stat = await this.fileService.resolve(location); + if (stat.children) { + const extensions = await Promise.all( + stat.children.filter(c => c.isDirectory) + .map(async c => { + if (isEqualOrParent(c.resource, this.userExtensionsLocation) && basename(c.resource).indexOf('.') === 0) { // Do not consider user extension folder starting with `.` + return null; + } + return this.scanExtension(c.resource, preferredType); + })); + return coalesce(extensions); + } + return []; + } + + private async scanDefaultSystemExtensions(): Promise { + this.logService.trace('Started scanning system extensions'); + const result = await this.scanExtensionsInLocation(this.systemExtensionsLocation, ExtensionType.System); + this.logService.trace('Scanned system extensions:', result.length); + return result; + } + + private async scanDevSystemExtensions(scanOptions: ScanOptions): Promise { + const devSystemExtensionsList = this.environmentService.isBuilt ? [] : this.productService.builtInExtensions; + if (!devSystemExtensionsList?.length) { + return []; + } + + this.logService.trace('Started scanning dev system extensions'); + const builtinExtensionControl = scanOptions.checkControlFile ? await this.getBuiltInExtensionControl() : {}; + const devSystemExtensionsLocations: URI[] = []; + for (const extension of devSystemExtensionsList) { + const controlState = builtinExtensionControl[extension.name] || 'marketplace'; + switch (controlState) { + case 'disabled': + break; + case 'marketplace': + devSystemExtensionsLocations.push(joinPath(this.devSystemExtensionsLocation, extension.name)); + break; + default: + devSystemExtensionsLocations.push(URI.file(controlState)); + break; + } + } + const result = await Promise.all(devSystemExtensionsLocations.map(location => this.scanExtension(location, ExtensionType.System))); + this.logService.trace('Scanned dev system extensions:', result.length); + return coalesce(result); + } + + private async getBuiltInExtensionControl(): Promise { + try { + const content = await this.fileService.readFile(joinPath(this.environmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json')); + return JSON.parse(content.value.toString()); + } catch (error) { + return {}; + } + } + + private async scanFromUserExtensionsLocation(): Promise { + return this.scanExtensionsInLocation(this.userExtensionsLocation, ExtensionType.User); + } + + private dedupExtensions(extensions: IRelaxedScannedExtension[], targetPlatform: TargetPlatform, pickLatest: boolean): IRelaxedScannedExtension[] { + const result = new Map(); + for (const extension of extensions) { + const extensionKey = ExtensionIdentifier.toKey(extension.identifier.id); + const existing = result.get(extensionKey); + if (existing) { + if (existing.isValid && !extension.isValid) { + continue; + } + if (existing.isValid === extension.isValid) { + if (pickLatest && semver.gt(existing.manifest.version, extension.manifest.version)) { + this.logService.debug(`Skipping extension ${extension.location.fsPath} with lower version ${extension.manifest.version}.`); + continue; + } + if (semver.eq(existing.manifest.version, extension.manifest.version) && existing.targetPlatform === targetPlatform) { + this.logService.debug(`Skipping extension ${extension.location.fsPath} from different target platform ${extension.targetPlatform}`); + continue; + } + } + if (existing.type === ExtensionType.System) { + this.logService.debug(`Overwriting system extension ${existing.location.fsPath} with ${extension.location.fsPath}.`); + } else { + this.logService.warn(`Overwriting user extension ${existing.location.fsPath} with ${extension.location.fsPath}.`); + } + } + result.set(extensionKey, extension); + } + return [...result.values()]; + } + + private joinErrors(errorOrErrors: (Error | string) | (Array)): Error { + const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors]; + if (errors.length === 1) { + return errors[0] instanceof Error ? errors[0] : new Error(errors[0]); + } + return errors.reduce((previousValue: Error, currentValue: Error | string) => { + return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`); + }, new Error('')); + } + + private _devSystemExtensionsLocation: URI | null = null; + private get devSystemExtensionsLocation(): URI { + if (!this._devSystemExtensionsLocation) { + this._devSystemExtensionsLocation = URI.file(path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions'))); + } + return this._devSystemExtensionsLocation; + } + + private async getUninstalledExtensions(): Promise> { + try { + const raw = (await this.fileService.readFile(joinPath(this.userExtensionsLocation, '.obsolete'))).value.toString(); + return JSON.parse(raw); + } catch (error) { + /* Ignore */ + } + return {}; + } + + private async scanExtension(extensionLocation: URI, preferredType: ExtensionType): Promise { + try { + const manifest = await this.scanExtensionManifest(extensionLocation); + if (manifest) { + // allow publisher to be undefined to make the initial extension authoring experience smoother + if (!manifest.publisher) { + manifest.publisher = UNDEFINED_PUBLISHER; + } + const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; + const metadata = manifest.__metadata; + delete manifest.__metadata; + const type = metadata?.isSystem ? ExtensionType.System : preferredType; + const isBuiltin = type === ExtensionType.System || !!metadata?.isBuiltin; + const validations = validateExtensionManifest(this.productService.version, this.productService.date, extensionLocation, manifest, isBuiltin); + let isValid = true; + for (const validation of validations) { + if (validation.severity === Severity.Error) { + isValid = false; + this.logService.error(this.formatMessage(extensionLocation, validation.message)); + } + } + return { + type, + identifier, + manifest, + location: extensionLocation, + isBuiltin, + targetPlatform: metadata?.targetPlatform ?? TargetPlatform.UNDEFINED, + metadata, + isValid, + validations + }; + } + } catch (e) { + if (preferredType !== ExtensionType.System) { + this.logService.error(e); + } + } + return null; + } + + private async scanExtensionManifest(extensionLocation: URI): Promise { + const manifestLocation = joinPath(extensionLocation, 'package.json'); + let content; + try { + content = (await this.fileService.readFile(manifestLocation)).value.toString(); + } catch (error) { + if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { + this.logService.error(this.formatMessage(extensionLocation, localize('fileReadFail', "Cannot read file {0}: {1}.", manifestLocation.path, error.message))); + } + return null; + } + let manifest: IScannedExtensionManifest; + try { + manifest = JSON.parse(content); + } catch (err) { + // invalid JSON, let's get good errors + const errors: ParseError[] = []; + parse(content, errors); + for (const e of errors) { + this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseFail', "Failed to parse {0}: [{1}, {2}] {3}.", manifestLocation.path, e.offset, e.length, getParseErrorMessage(e.error)))); + } + return null; + } + if (getNodeType(manifest) !== 'object') { + this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not an JSON object.", manifestLocation.path))); + return null; + } + return manifest; + } + + private async localizeExtensions(extensions: IRelaxedScannedExtension[], nlsConfig?: NlsConfiguration): Promise { + await Promise.all(extensions.map(async extension => { + try { + extension.manifest = await this.localizeManifest(extension.location, extension.manifest, nlsConfig); + } catch (error) { + /* Ignore Error */ + } + })); + return extensions; + } + + private async localizeManifest(extensionLocation: URI, extensionManifest: IExtensionManifest, nlsConfig?: NlsConfiguration): Promise { + const localizedMessages = await this.getLocalizedMessages(extensionLocation, extensionManifest, nlsConfig); + if (localizedMessages) { + try { + const errors: ParseError[] = []; + // resolveOriginalMessageBundle returns null if localizedMessages.default === undefined; + const defaults = await this.resolveOriginalMessageBundle(localizedMessages.default, errors); + if (errors.length > 0) { + errors.forEach((error) => { + this.logService.error(this.formatMessage(extensionLocation, localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localizedMessages.default?.path, getParseErrorMessage(error.error)))); + }); + return extensionManifest; + } else if (getNodeType(localizedMessages) !== 'object') { + this.logService.error(this.formatMessage(extensionLocation, localize('jsonInvalidFormat', "Invalid format {0}: JSON object expected.", localizedMessages.default?.path))); + return extensionManifest; + } + const localized = localizedMessages.values || Object.create(null); + this.replaceNLStrings(!!nlsConfig?.pseudo, extensionManifest, localized, defaults, extensionLocation); + } catch (error) { + /*Ignore Error*/ + } + } + return extensionManifest; + } + + private async getLocalizedMessages(extensionLocation: URI, extensionManifest: IExtensionManifest, nlsConfig?: NlsConfiguration): Promise { + const defaultPackageNLS = joinPath(extensionLocation, 'package.nls.json'); + if (!nlsConfig) { + return { values: undefined, default: defaultPackageNLS }; + } + + const reportErrors = (localized: URI | null, errors: ParseError[]): void => { + errors.forEach((error) => { + this.logService.error(this.formatMessage(extensionLocation, localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localized?.path, getParseErrorMessage(error.error)))); + }); + }; + const reportInvalidFormat = (localized: URI | null): void => { + this.logService.error(this.formatMessage(extensionLocation, localize('jsonInvalidFormat', "Invalid format {0}: JSON object expected.", localized?.path))); + }; + + const translationId = `${extensionManifest.publisher}.${extensionManifest.name}`; + const translationPath = nlsConfig.translations[translationId]; + + if (translationPath) { + try { + const translationResource = URI.file(translationPath); + const content = (await this.fileService.readFile(translationResource)).value.toString(); + let errors: ParseError[] = []; + let translationBundle: TranslationBundle = parse(content, errors); + if (errors.length > 0) { + reportErrors(translationResource, errors); + return { values: undefined, default: defaultPackageNLS }; + } else if (getNodeType(translationBundle) !== 'object') { + reportInvalidFormat(translationResource); + return { values: undefined, default: defaultPackageNLS }; + } else { + let values = translationBundle.contents ? translationBundle.contents.package : undefined; + return { values: values, default: defaultPackageNLS }; + } + } catch (error) { + return { values: undefined, default: defaultPackageNLS }; + } + } else { + const exists = await this.fileService.exists(defaultPackageNLS); + if (!exists) { + return undefined; + } + let messageBundle; + try { + messageBundle = await this.findMessageBundles(nlsConfig, extensionLocation); + } catch (error) { + return undefined; + } + if (!messageBundle.localized) { + return { values: undefined, default: messageBundle.original }; + } + try { + const messageBundleContent = (await this.fileService.readFile(messageBundle.localized)).value.toString(); + let errors: ParseError[] = []; + let messages: MessageBag = parse(messageBundleContent, errors); + if (errors.length > 0) { + reportErrors(messageBundle.localized, errors); + return { values: undefined, default: messageBundle.original }; + } else if (getNodeType(messages) !== 'object') { + reportInvalidFormat(messageBundle.localized); + return { values: undefined, default: messageBundle.original }; + } + return { values: messages, default: messageBundle.original }; + } catch (error) { + return { values: undefined, default: messageBundle.original }; + } + } + } + + /** + * Parses original message bundle, returns null if the original message bundle is null. + */ + private async resolveOriginalMessageBundle(originalMessageBundle: URI | null, errors: ParseError[]): Promise<{ [key: string]: string } | null> { + if (originalMessageBundle) { + try { + const originalBundleContent = (await this.fileService.readFile(originalMessageBundle)).value.toString(); + return parse(originalBundleContent, errors); + } catch (error) { + /* Ignore Error */ + return null; + } + } else { + return null; + } + } + + /** + * Finds localized message bundle and the original (unlocalized) one. + * If the localized file is not present, returns null for the original and marks original as localized. + */ + private findMessageBundles(nlsConfig: NlsConfiguration, extensionLocation: URI): Promise<{ localized: URI; original: URI | null }> { + return new Promise<{ localized: URI; original: URI | null }>((c, e) => { + const loop = (locale: string): void => { + let toCheck = joinPath(extensionLocation, `package.nls.${locale}.json`); + this.fileService.exists(toCheck).then(exists => { + if (exists) { + c({ localized: toCheck, original: joinPath(extensionLocation, 'package.nls.json') }); + } + let index = locale.lastIndexOf('-'); + if (index === -1) { + c({ localized: joinPath(extensionLocation, 'package.nls.json'), original: null }); + } else { + locale = locale.substring(0, index); + loop(locale); + } + }); + }; + if (nlsConfig.devMode || nlsConfig.pseudo || !nlsConfig.locale) { + return c({ localized: joinPath(extensionLocation, 'package.nls.json'), original: null }); + } + loop(nlsConfig.locale); + }); + } + + /** + * This routine makes the following assumptions: + * The root element is an object literal + */ + private replaceNLStrings(pseudo: boolean, literal: T, messages: MessageBag, originalMessages: MessageBag | null, extensionLocation: URI): void { + const processEntry = (obj: any, key: string | number, command?: boolean) => { + const value = obj[key]; + if (isString(value)) { + const str = value; + const length = str.length; + if (length > 1 && str[0] === '%' && str[length - 1] === '%') { + const messageKey = str.substr(1, length - 2); + let translated = messages[messageKey]; + // If the messages come from a language pack they might miss some keys + // Fill them from the original messages. + if (translated === undefined && originalMessages) { + translated = originalMessages[messageKey]; + } + let message: string | undefined = typeof translated === 'string' ? translated : (typeof translated?.message === 'string' ? translated.message : undefined); + if (message !== undefined) { + if (pseudo) { + // FF3B and FF3D is the Unicode zenkaku representation for [ and ] + message = '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D'; + } + obj[key] = command && (key === 'title' || key === 'category') && originalMessages ? { value: message, original: originalMessages[messageKey] } : message; + } else { + this.logService.warn(this.formatMessage(extensionLocation, localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey))); + } + } + } else if (isObject(value)) { + for (let k in value) { + if (value.hasOwnProperty(k)) { + k === 'commands' ? processEntry(value, k, true) : processEntry(value, k, command); + } + } + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + processEntry(value, i, command); + } + } + }; + + for (let key in literal) { + if (literal.hasOwnProperty(key)) { + processEntry(literal, key); + } + } + } + + private formatMessage(extensionLocation: URI, message: string): string { + return `[${extensionLocation.path}]: ${message}`; + } +} + +export function toExtensionDescription(extension: IScannedExtension, isUnderDevelopment: boolean): IExtensionDescription { + const id = getExtensionId(extension.manifest.publisher, extension.manifest.name); + return { + id, + identifier: new ExtensionIdentifier(id), + isBuiltin: extension.type === ExtensionType.System, + isUserBuiltin: extension.type === ExtensionType.User && extension.isBuiltin, + isUnderDevelopment, + extensionLocation: extension.location, + uuid: extension.identifier.uuid, + targetPlatform: extension.targetPlatform, + ...extension.manifest, + }; +} + +registerSingleton(INativeExtensionsScannerService, NativeExtensionsScannerService); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 0a0254ea806..81f5764cb47 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -29,7 +29,7 @@ import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/exten import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle'; import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache'; -import { ExtensionsScanner, ILocalExtensionManifest } from 'vs/platform/extensionManagement/node/extensionsScanner'; +import { ExtensionsScanner } from 'vs/platform/extensionManagement/node/extensionsScanner'; import { ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; @@ -65,7 +65,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi ) { super(galleryService, telemetryService, logService, productService); const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle)); - this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension), this.getTargetPlatform())); + this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension))); this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this)); this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader)); const extensionsWatcher = this._register(new ExtensionsWatcher(this, fileService, environmentService, logService, uriIdentityService)); @@ -123,18 +123,18 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise { this.logService.trace('ExtensionManagementService#updateMetadata', local.identifier.id); - const localMetadata: Metadata = { ...((local.manifest).__metadata || {}), ...metadata }; + const localMetadata: Metadata = { ...metadata }; if (metadata.isPreReleaseVersion) { localMetadata.preRelease = true; } - local = await this.extensionsScanner.saveMetadataForLocalExtension(local, localMetadata); + local = await this.extensionsScanner.updateMetadata(local, localMetadata); this.manifestCache.invalidate(); return local; } async updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise { this.logService.trace('ExtensionManagementService#updateExtensionScope', local.identifier.id); - local = await this.extensionsScanner.saveMetadataForLocalExtension(local, { ...((local.manifest).__metadata || {}), isMachineScoped }); + local = await this.extensionsScanner.updateMetadata(local, { isMachineScoped }); this.manifestCache.invalidate(); return local; } @@ -206,7 +206,7 @@ abstract class AbstractInstallExtensionTask extends AbstractExtensionTasklocal.manifest).__metadata || {}), ...installableExtension.metadata }) : local; + return installableExtension.metadata ? this.extensionsScanner.updateMetadata(local, installableExtension.metadata) : local; } } catch (e) { if (isMacintosh) { diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts index 6945e43052e..c47171faf08 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -4,52 +4,40 @@ *--------------------------------------------------------------------------------------------*/ import { flatten } from 'vs/base/common/arrays'; -import { Limiter, Promises, Queue } from 'vs/base/common/async'; +import { Promises, Queue } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStringDictionary } from 'vs/base/common/collections'; import { getErrorMessage } from 'vs/base/common/errors'; import { Disposable } from 'vs/base/common/lifecycle'; -import { FileAccess } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; -import { basename, isEqualOrParent, joinPath } from 'vs/base/common/resources'; +import { joinPath } from 'vs/base/common/resources'; import * as semver from 'vs/base/common/semver/semver'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as pfs from 'vs/base/node/pfs'; import { extract, ExtractError } from 'vs/base/node/zip'; import { localize } from 'vs/nls'; -import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { ExtensionManagementError, ExtensionManagementErrorCode, Metadata, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; -import { ExtensionType, IExtensionIdentifier, ExtensionIdentifier, IExtensionManifest, TargetPlatform, UNDEFINED_PUBLISHER } from 'vs/platform/extensions/common/extensions'; +import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { INativeExtensionsScannerService, IScannedExtension, ScanOptions } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProductService } from 'vs/platform/product/common/productService'; - -export type ILocalExtensionManifest = IExtensionManifest & { __metadata?: Metadata }; -type IRelaxedLocalExtension = ILocalExtension & { type: ExtensionType; isBuiltin: boolean; targetPlatform: TargetPlatform }; export class ExtensionsScanner extends Disposable { - private readonly systemExtensionsLocation: URI; - private readonly userExtensionsLocation: URI; private readonly uninstalledPath: string; private readonly uninstalledFileLimiter: Queue; constructor( private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise, - private readonly targetPlatform: Promise, @IFileService private readonly fileService: IFileService, + @INativeExtensionsScannerService private readonly extensionsScannerService: INativeExtensionsScannerService, @ILogService private readonly logService: ILogService, - @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, - @IProductService private readonly productService: IProductService, ) { super(); - this.systemExtensionsLocation = URI.file(environmentService.builtinExtensionsPath); - this.userExtensionsLocation = URI.file(environmentService.extensionsPath); - this.uninstalledPath = joinPath(this.userExtensionsLocation, '.obsolete').fsPath; + this.uninstalledPath = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete').fsPath; this.uninstalledFileLimiter = new Queue(); } @@ -59,39 +47,29 @@ export class ExtensionsScanner extends Disposable { } async scanExtensions(type: ExtensionType | null): Promise { - const promises: Promise[] = []; - - if (type === null || type === ExtensionType.System) { - promises.push(this.scanDefaultSystemExtensions()); - promises.push(this.environmentService.isBuilt ? Promise.resolve([]) : this.scanDevSystemExtensions()); - } else { - promises.push(Promise.resolve([])); - promises.push(Promise.resolve([])); - } - promises.push(this.scanUserExtensions(false)); - - try { - const [defaultSystemExtensions, devSystemExtensions, userExtensions] = await Promise.all(promises); - const result = this.dedupExtensions([...defaultSystemExtensions, ...devSystemExtensions, ...userExtensions], await this.targetPlatform); - return type !== null ? result.filter(r => r.type === type) : result; - } catch (error) { - throw this.joinErrors(error); + const scannedOptions: ScanOptions = { includeInvalid: true }; + let scannedExtensions: IScannedExtension[] = []; + if (type === null) { + scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions(scannedOptions)); + } else if (type === ExtensionType.System) { + scannedExtensions.push(...await this.extensionsScannerService.scanSystemExtensions(scannedOptions)); + scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(scannedOptions)); + } else if (type === ExtensionType.User) { + scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(scannedOptions)); } + scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; + return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); } async scanUserExtensions(excludeOutdated: boolean): Promise { - this.logService.trace('Started scanning user extensions'); - let [uninstalled, extensions] = await Promise.all([this.getUninstalledExtensions(), this.scanFromUserExtensionsLocation()]); - extensions = extensions.filter(e => !uninstalled[ExtensionKey.create(e).toString()]); - extensions = excludeOutdated ? this.dedupExtensions(extensions, await this.targetPlatform) : extensions; - this.logService.trace('Scanned user extensions:', extensions.length); - return extensions; + const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); + return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); } async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, metadata: Metadata | undefined, token: CancellationToken): Promise { const folderName = extensionKey.toString(); - const tempPath = path.join(this.userExtensionsLocation.fsPath, `.${generateUuid()}`); - const extensionPath = path.join(this.userExtensionsLocation.fsPath, folderName); + const tempPath = path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, `.${generateUuid()}`); + const extensionPath = path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, folderName); try { await pfs.Promises.rm(extensionPath); @@ -100,11 +78,7 @@ export class ExtensionsScanner extends Disposable { } await this.extractAtLocation(extensionKey, zipPath, tempPath, token); - let local = await this.scanExtension(URI.file(tempPath), ExtensionType.User); - if (!local) { - throw new Error(localize('cannot read', "Cannot read the extension from {0}", tempPath)); - } - await this.storeMetadata(local, { ...metadata, installedTimestamp: Date.now() }); + await this.extensionsScannerService.updateMetadata(URI.file(tempPath), { ...metadata, installedTimestamp: Date.now() }); try { await this.rename(extensionKey, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */); @@ -121,20 +95,12 @@ export class ExtensionsScanner extends Disposable { } } - try { - local = await this.scanExtension(URI.file(extensionPath), ExtensionType.User); - } catch (e) { /*ignore */ } - - if (local) { - return local; - } - throw new Error(localize('cannot read', "Cannot read the extension from {0}", this.userExtensionsLocation.fsPath)); + return this.scanLocalExtension(URI.file(extensionPath), ExtensionType.User); } - async saveMetadataForLocalExtension(local: ILocalExtension, metadata: Metadata): Promise { - this.setMetadata(local, metadata); - await this.storeMetadata(local, { ...metadata, installedTimestamp: local.installedTimestamp }); - return local; + async updateMetadata(local: ILocalExtension, metadata: Partial): Promise { + await this.extensionsScannerService.updateMetadata(local.location, metadata); + return this.scanLocalExtension(local.location, local.type); } getUninstalledExtensions(): Promise> { @@ -155,34 +121,20 @@ export class ExtensionsScanner extends Disposable { if (!localExtension) { return null; } - await this.storeMetadata(localExtension, { installedTimestamp: Date.now() }); - return this.scanExtension(localExtension.location, localExtension.type); + return this.updateMetadata(localExtension, { installedTimestamp: Date.now() }); } - async removeExtension(extension: ILocalExtension, type: string): Promise { + async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath); await pfs.Promises.rm(extension.location.fsPath); this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath); } - async removeUninstalledExtension(extension: ILocalExtension): Promise { + async removeUninstalledExtension(extension: ILocalExtension | IScannedExtension): Promise { await this.removeExtension(extension, 'uninstalled'); await this.withUninstalledExtensions(uninstalled => delete uninstalled[ExtensionKey.create(extension).toString()]); } - private async storeMetadata(local: ILocalExtension, metaData: Metadata): Promise { - // unset if false - metaData.isMachineScoped = metaData.isMachineScoped || undefined; - metaData.isBuiltin = metaData.isBuiltin || undefined; - metaData.installedTimestamp = metaData.installedTimestamp || undefined; - const manifestPath = path.join(local.location.fsPath, 'package.json'); - const raw = await pfs.Promises.readFile(manifestPath, 'utf8'); - const { manifest } = await this.parseManifest(raw); - (manifest as ILocalExtensionManifest).__metadata = metaData; - await pfs.Promises.writeFile(manifestPath, JSON.stringify(manifest, null, '\t')); - return local; - } - private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; @@ -253,131 +205,69 @@ export class ExtensionsScanner extends Disposable { } } - private async scanExtensionsInLocation(location: URI, preferredType: ExtensionType): Promise { - const limiter = new Limiter(10); - const stat = await this.fileService.resolve(location); - if (stat.children) { - const extensions = await Promise.all(stat.children.filter(c => c.isDirectory) - .map(c => limiter.queue(async () => { - if (isEqualOrParent(c.resource, this.userExtensionsLocation) && basename(c.resource).indexOf('.') === 0) { // Do not consider user extension folder starting with `.` - return null; - } - return this.scanExtension(c.resource, preferredType); - }))); - return extensions.filter(e => e && e.identifier); + private async scanLocalExtension(location: URI, type: ExtensionType): Promise { + const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true }); + if (scannedExtension) { + return this.toLocalExtension(scannedExtension); } - return []; + throw new Error(localize('cannot read', "Cannot read the extension from {0}", location.path)); } - private async scanExtension(extensionLocation: URI, preferredType: ExtensionType): Promise { - try { - const stat = await this.fileService.resolve(extensionLocation); - if (stat.children) { - const { manifest, metadata } = await this.readManifest(extensionLocation.fsPath); - const readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource; - const changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource; - const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; - const type = metadata?.isSystem ? ExtensionType.System : preferredType; - const local = { type, identifier, manifest, location: extensionLocation, readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System }; - this.setMetadata(local, metadata); - return local; - } - } catch (e) { - if (preferredType !== ExtensionType.System) { - this.logService.trace(e); - } - } - return null; - } - - private async scanDefaultSystemExtensions(): Promise { - this.logService.trace('Started scanning system extensions'); - const result = await this.scanExtensionsInLocation(this.systemExtensionsLocation, ExtensionType.System); - this.logService.trace('Scanned system extensions:', result.length); - return result; - } - - private async scanDevSystemExtensions(): Promise { - this.logService.trace('Started scanning dev system extensions'); - const devSystemExtensionsList = this.getDevSystemExtensionsList(); - if (devSystemExtensionsList.length) { - const result = await this.scanExtensionsInLocation(this.devSystemExtensionsLocation, ExtensionType.System); - this.logService.trace('Scanned dev system extensions:', result.length); - return result.filter(r => devSystemExtensionsList.some(id => areSameExtensions(r.identifier, { id }))); - } else { - return []; - } - } - - private async scanFromUserExtensionsLocation(): Promise { - return this.scanExtensionsInLocation(this.userExtensionsLocation, ExtensionType.User); - } - - private dedupExtensions(extensions: ILocalExtension[], targetPlatform: TargetPlatform): ILocalExtension[] { - const result = new Map(); - for (const extension of extensions) { - const extensionKey = ExtensionIdentifier.toKey(extension.identifier.id); - const existing = result.get(extensionKey); - if (existing) { - if (semver.gt(existing.manifest.version, extension.manifest.version)) { - this.logService.debug(`Skipping extension ${extension.location.fsPath} with lower version ${extension.manifest.version}.`); - continue; - } - if (semver.eq(existing.manifest.version, extension.manifest.version) && existing.targetPlatform === targetPlatform) { - this.logService.debug(`Skipping extension ${extension.location.fsPath} from different target platform ${extension.targetPlatform}`); - continue; - } - if (existing.type === ExtensionType.System) { - this.logService.debug(`Overwriting system extension ${existing.location.fsPath} with ${extension.location.fsPath}.`); - } else { - this.logService.warn(`Overwriting user extension ${existing.location.fsPath} with ${extension.location.fsPath}.`); - } - } - result.set(extensionKey, extension); + private async toLocalExtension(extension: IScannedExtension): Promise { + const stat = await this.fileService.resolve(extension.location); + let readmeUrl: URI | undefined; + let changelogUrl: URI | undefined; + if (stat.children) { + readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource; + changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource; } - return [...result.values()]; - } - - private setMetadata(local: IRelaxedLocalExtension, metadata: Metadata | null): void { - local.publisherDisplayName = metadata?.publisherDisplayName || null; - local.publisherId = metadata?.publisherId || null; - local.identifier.uuid = metadata?.id; - local.isMachineScoped = !!metadata?.isMachineScoped; - local.isPreReleaseVersion = !!metadata?.isPreReleaseVersion; - local.preRelease = !!metadata?.preRelease; - local.isBuiltin = local.type === ExtensionType.System || !!metadata?.isBuiltin; - local.installedTimestamp = metadata?.installedTimestamp; - local.targetPlatform = metadata?.targetPlatform ?? TargetPlatform.UNDEFINED; - local.updated = !!metadata?.updated; + return { + identifier: extension.identifier, + type: extension.type, + isBuiltin: extension.isBuiltin || !!extension.metadata?.isBuiltin, + location: extension.location, + manifest: extension.manifest, + targetPlatform: extension.targetPlatform, + validations: extension.validations, + isValid: extension.isValid, + readmeUrl, + changelogUrl, + publisherDisplayName: extension.metadata?.publisherDisplayName || null, + publisherId: extension.metadata?.publisherId || null, + isMachineScoped: !!extension.metadata?.isMachineScoped, + isPreReleaseVersion: !!extension.metadata?.isPreReleaseVersion, + preRelease: !!extension.metadata?.preRelease, + installedTimestamp: extension.metadata?.installedTimestamp, + updated: !!extension.metadata?.updated, + }; } - private async removeUninstalledExtensions(): Promise { const uninstalled = await this.getUninstalledExtensions(); - const extensions = await this.scanFromUserExtensionsLocation(); // All user extensions + const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions const installed: Set = new Set(); for (const e of extensions) { if (!uninstalled[ExtensionKey.create(e).toString()]) { installed.add(e.identifier.id.toLowerCase()); } } - const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier); + const byExtension = groupByExtension(extensions, e => e.identifier); await Promises.settled(byExtension.map(async e => { const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]; if (!installed.has(latest.identifier.id.toLowerCase())) { - await this.beforeRemovingExtension(latest); + await this.beforeRemovingExtension(await this.toLocalExtension(latest)); } })); - const toRemove: ILocalExtension[] = extensions.filter(e => uninstalled[ExtensionKey.create(e).toString()]); + const toRemove = extensions.filter(e => uninstalled[ExtensionKey.create(e).toString()]); await Promises.settled(toRemove.map(e => this.removeUninstalledExtension(e))); } private async removeOutdatedExtensions(): Promise { - const extensions = await this.scanFromUserExtensionsLocation(); - const toRemove: ILocalExtension[] = []; + const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions + const toRemove: IScannedExtension[] = []; // Outdated extensions - const targetPlatform = await this.targetPlatform; - const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier); + const targetPlatform = await this.extensionsScannerService.getTargetPlatform(); + const byExtension = groupByExtension(extensions, e => e.identifier); toRemove.push(...flatten(byExtension.map(p => p.sort((a, b) => { const vcompare = semver.rcompare(a.manifest.version, b.manifest.version); if (vcompare !== 0) { @@ -392,10 +282,6 @@ export class ExtensionsScanner extends Disposable { await Promises.settled(toRemove.map(extension => this.removeExtension(extension, 'outdated'))); } - private getDevSystemExtensionsList(): string[] { - return (this.productService.builtInExtensions || []).map(e => e.name); - } - private joinErrors(errorOrErrors: (Error | string) | (Array)): Error { const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors]; if (errors.length === 1) { @@ -406,43 +292,4 @@ export class ExtensionsScanner extends Disposable { }, new Error('')); } - private _devSystemExtensionsLocation: URI | null = null; - private get devSystemExtensionsLocation(): URI { - if (!this._devSystemExtensionsLocation) { - this._devSystemExtensionsLocation = URI.file(path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions'))); - } - return this._devSystemExtensionsLocation; - } - - private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: Metadata | null }> { - const promises = [ - pfs.Promises.readFile(path.join(extensionPath, 'package.json'), 'utf8') - .then(raw => this.parseManifest(raw)), - pfs.Promises.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8') - .then(undefined, err => err.code !== 'ENOENT' ? Promise.reject(err) : '{}') - .then(raw => JSON.parse(raw)) - ]; - - const [{ manifest, metadata }, translations] = await Promise.all(promises); - return { - manifest: localizeManifest(manifest, translations), - metadata - }; - } - - private parseManifest(raw: string): Promise<{ manifest: IExtensionManifest; metadata: Metadata | null }> { - return new Promise((c, e) => { - try { - const manifest = JSON.parse(raw); - // allow publisher to be undefined to make the initial extension authoring experience smoother - if (!manifest.publisher) { - manifest.publisher = UNDEFINED_PUBLISHER; - } - const metadata = manifest.__metadata || null; - c({ manifest, metadata }); - } catch (err) { - e(new Error(localize('invalidManifest', "Extension invalid: package.json is not a JSON file."))); - } - }); - } } diff --git a/src/vs/platform/extensions/common/extensionValidator.ts b/src/vs/platform/extensions/common/extensionValidator.ts index dfeea91105f..628ecbacc83 100644 --- a/src/vs/platform/extensions/common/extensionValidator.ts +++ b/src/vs/platform/extensions/common/extensionValidator.ts @@ -3,7 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isEqualOrParent, joinPath } from 'vs/base/common/resources'; +import Severity from 'vs/base/common/severity'; +import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; +import * as semver from 'vs/base/common/semver/semver'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; export interface IParsedVersion { @@ -235,6 +239,98 @@ export function isValidVersion(_inputVersion: string | INormalizedVersion, _inpu type ProductDate = string | Date | undefined; +export function validateExtensionManifest(productVersion: string, productDate: ProductDate, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean): { severity: Severity; message: string }[] { + const validations: { severity: Severity; message: string }[] = []; + if (typeof extensionManifest.publisher !== 'undefined' && typeof extensionManifest.publisher !== 'string') { + validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.publisher', "property publisher must be of type `string`.") }); + return validations; + } + if (typeof extensionManifest.name !== 'string') { + validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.name', "property `{0}` is mandatory and must be of type `string`", 'name') }); + return validations; + } + if (typeof extensionManifest.version !== 'string') { + validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.version', "property `{0}` is mandatory and must be of type `string`", 'version') }); + return validations; + } + if (!extensionManifest.engines) { + validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.engines', "property `{0}` is mandatory and must be of type `object`", 'engines') }); + return validations; + } + if (typeof extensionManifest.engines.vscode !== 'string') { + validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.engines.vscode', "property `{0}` is mandatory and must be of type `string`", 'engines.vscode') }); + return validations; + } + if (typeof extensionManifest.extensionDependencies !== 'undefined') { + if (!isStringArray(extensionManifest.extensionDependencies)) { + validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.extensionDependencies', "property `{0}` can be omitted or must be of type `string[]`", 'extensionDependencies') }); + return validations; + } + } + if (typeof extensionManifest.activationEvents !== 'undefined') { + if (!isStringArray(extensionManifest.activationEvents)) { + validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.activationEvents1', "property `{0}` can be omitted or must be of type `string[]`", 'activationEvents') }); + return validations; + } + if (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined') { + validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.activationEvents2', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main') }); + return validations; + } + } + if (typeof extensionManifest.extensionKind !== 'undefined') { + if (typeof extensionManifest.main === 'undefined') { + validations.push({ severity: Severity.Warning, message: nls.localize('extensionDescription.extensionKind', "property `{0}` can be defined only if property `main` is also defined.", 'extensionKind') }); + // not a failure case + } + } + if (typeof extensionManifest.main !== 'undefined') { + if (typeof extensionManifest.main !== 'string') { + validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main') }); + return validations; + } else { + const mainLocation = joinPath(extensionLocation, extensionManifest.main); + if (!isEqualOrParent(mainLocation, extensionLocation)) { + validations.push({ severity: Severity.Warning, message: nls.localize('extensionDescription.main2', "Expected `main` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", mainLocation.path, extensionLocation.path) }); + // not a failure case + } + } + if (typeof extensionManifest.activationEvents === 'undefined') { + validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.main3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main') }); + return validations; + } + } + if (typeof extensionManifest.browser !== 'undefined') { + if (typeof extensionManifest.browser !== 'string') { + validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.browser1', "property `{0}` can be omitted or must be of type `string`", 'browser') }); + return validations; + } else { + const browserLocation = joinPath(extensionLocation, extensionManifest.browser); + if (!isEqualOrParent(browserLocation, extensionLocation)) { + validations.push({ severity: Severity.Warning, message: nls.localize('extensionDescription.browser2', "Expected `browser` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", browserLocation.path, extensionLocation.path) }); + // not a failure case + } + } + if (typeof extensionManifest.activationEvents === 'undefined') { + validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.browser3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'browser') }); + return validations; + } + } + + if (!semver.valid(extensionManifest.version)) { + validations.push({ severity: Severity.Error, message: nls.localize('notSemver', "Extension version is not semver compatible.") }); + return validations; + } + + const notices: string[] = []; + const isValid = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices); + if (!isValid) { + for (const notice of notices) { + validations.push({ severity: Severity.Error, message: notice }); + } + } + return validations; +} + export function isValidExtensionVersion(productVersion: string, productDate: ProductDate, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, notices: string[]): boolean { if (extensionIsBuiltin || (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined')) { @@ -282,3 +378,15 @@ function isVersionValid(currentVersion: string, date: ProductDate, requestedVers return true; } + +function isStringArray(arr: string[]): boolean { + if (!Array.isArray(arr)) { + return false; + } + for (let i = 0, len = arr.length; i < len; i++) { + if (typeof arr[i] !== 'string') { + return false; + } + } + return true; +} diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 0bfebc689dd..eda46215228 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import Severity from 'vs/base/common/severity'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { ExtensionKind } from 'vs/platform/environment/common/environment'; @@ -320,6 +321,8 @@ export interface IExtension { readonly targetPlatform: TargetPlatform; readonly readmeUrl?: URI; readonly changelogUrl?: URI; + readonly isValid: boolean; + readonly validations: readonly { readonly severity: Severity; readonly message: string }[]; } /** diff --git a/src/vs/server/node/remoteAgentEnvironmentImpl.ts b/src/vs/server/node/remoteAgentEnvironmentImpl.ts index 4b8502beb39..28dda15bd92 100644 --- a/src/vs/server/node/remoteAgentEnvironmentImpl.ts +++ b/src/vs/server/node/remoteAgentEnvironmentImpl.ts @@ -10,11 +10,10 @@ import { URI } from 'vs/base/common/uri'; import { createURITransformer } from 'vs/workbench/api/node/uriTransformer'; import { IRemoteAgentEnvironmentDTO, IGetEnvironmentDataArguments, IScanExtensionsArguments, IScanSingleExtensionArguments, IGetExtensionHostExitInfoArguments } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel'; import * as nls from 'vs/nls'; -import { FileAccess, Schemas } from 'vs/base/common/network'; +import { Schemas } from 'vs/base/common/network'; import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; -import { Translations, ExtensionScanner, ExtensionScannerInput, IExtensionResolver, IExtensionReference } from 'vs/workbench/services/extensions/common/extensionPoints'; import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { transformOutgoingURIs } from 'vs/base/common/uriIpc'; import { ILogService } from 'vs/platform/log/common/log'; import { getNLSConfiguration, InternalNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; @@ -22,31 +21,14 @@ import { ContextKeyExpr, ContextKeyDefinedExpr, ContextKeyNotExpr, ContextKeyEqu import { listProcesses } from 'vs/base/node/ps'; import { getMachineInfo, collectWorkspaceStats } from 'vs/platform/diagnostics/node/diagnosticsService'; import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; -import { basename, isAbsolute, join, normalize } from 'vs/base/common/path'; +import { basename, isAbsolute, join, resolve } from 'vs/base/common/path'; import { ProcessItem } from 'vs/base/common/processes'; -import { IBuiltInExtension } from 'vs/base/common/product'; -import { IExtensionManagementCLIService, IExtensionManagementService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementCLIService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { cwd } from 'vs/base/common/process'; import * as pfs from 'vs/base/node/pfs'; -import { IProductService } from 'vs/platform/product/common/productService'; import { ServerConnectionToken, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken'; import { IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; -import { IFileService } from 'vs/platform/files/common/files'; - -let _SystemExtensionsRoot: string | null = null; -function getSystemExtensionsRoot(): string { - if (!_SystemExtensionsRoot) { - _SystemExtensionsRoot = normalize(join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions')); - } - return _SystemExtensionsRoot; -} -let _ExtraDevSystemExtensionsRoot: string | null = null; -function getExtraDevSystemExtensionsRoot(): string { - if (!_ExtraDevSystemExtensionsRoot) { - _ExtraDevSystemExtensionsRoot = normalize(join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions')); - } - return _ExtraDevSystemExtensionsRoot; -} +import { INativeExtensionsScannerService, NlsConfiguration, toExtensionDescription, Translations } from 'vs/platform/extensionManagement/common/extensionsScannerService'; export class RemoteAgentEnvironmentChannel implements IServerChannel { @@ -56,31 +38,29 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { constructor( private readonly _connectionToken: ServerConnectionToken, - private readonly environmentService: IServerEnvironmentService, + private readonly _environmentService: IServerEnvironmentService, extensionManagementCLIService: IExtensionManagementCLIService, - private readonly _extensionManagementService: IExtensionManagementService, - private readonly logService: ILogService, - private readonly productService: IProductService, - private readonly extensionHostStatusService: IExtensionHostStatusService, - private readonly _fileService: IFileService, + private readonly _logService: ILogService, + private readonly _extensionHostStatusService: IExtensionHostStatusService, + private readonly _extensionsScannerService: INativeExtensionsScannerService, ) { - if (environmentService.args['install-builtin-extension']) { - const installOptions: InstallOptions = { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] }; - this.whenExtensionsReady = extensionManagementCLIService.installExtensions([], environmentService.args['install-builtin-extension'], installOptions, !!environmentService.args['force']) + if (_environmentService.args['install-builtin-extension']) { + const installOptions: InstallOptions = { isMachineScoped: !!_environmentService.args['do-not-sync'], installPreReleaseVersion: !!_environmentService.args['pre-release'] }; + this.whenExtensionsReady = extensionManagementCLIService.installExtensions([], _environmentService.args['install-builtin-extension'], installOptions, !!_environmentService.args['force']) .then(null, error => { - logService.error(error); + _logService.error(error); }); } else { this.whenExtensionsReady = Promise.resolve(); } - const extensionsToInstall = environmentService.args['install-extension']; + const extensionsToInstall = _environmentService.args['install-extension']; if (extensionsToInstall) { const idsOrVSIX = extensionsToInstall.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input); this.whenExtensionsReady - .then(() => extensionManagementCLIService.installExtensions(idsOrVSIX, [], { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] }, !!environmentService.args['force'])) + .then(() => extensionManagementCLIService.installExtensions(idsOrVSIX, [], { isMachineScoped: !!_environmentService.args['do-not-sync'], installPreReleaseVersion: !!_environmentService.args['pre-release'] }, !!_environmentService.args['force'])) .then(null, error => { - logService.error(error); + _logService.error(error); }); } } @@ -100,7 +80,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { case 'getExtensionHostExitInfo': { const args = arg; - return this.extensionHostStatusService.getExitInfo(args.reconnectionToken); + return this._extensionHostStatusService.getExitInfo(args.reconnectionToken); } case 'whenExtensionsReady': { @@ -112,7 +92,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { await this.whenExtensionsReady; const args = arg; const language = args.language; - this.logService.trace(`Scanning extensions using UI language: ${language}`); + this._logService.trace(`Scanning extensions using UI language: ${language}`); const uriTransformer = createURITransformer(args.remoteAuthority); const extensionDevelopmentLocations = args.extensionDevelopmentPath && args.extensionDevelopmentPath.map(url => URI.revive(uriTransformer.transformIncoming(url))); @@ -121,7 +101,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { let extensions = await this._scanExtensions(language, extensionDevelopmentPath); extensions = transformOutgoingURIs(extensions, uriTransformer); - this.logService.trace('Scanned Extensions', extensions); + this._logService.trace('Scanned Extensions', extensions); RemoteAgentEnvironmentChannel._massageWhenConditions(extensions); return extensions; @@ -308,24 +288,24 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { return { pid: process.pid, connectionToken: (this._connectionToken.type !== ServerConnectionTokenType.None ? this._connectionToken.value : ''), - appRoot: URI.file(this.environmentService.appRoot), - settingsPath: this.environmentService.machineSettingsResource, - logsPath: URI.file(this.environmentService.logsPath), - extensionsPath: URI.file(this.environmentService.extensionsPath!), - extensionHostLogsPath: URI.file(join(this.environmentService.logsPath, `exthost${RemoteAgentEnvironmentChannel._namePool++}`)), - globalStorageHome: this.environmentService.globalStorageHome, - workspaceStorageHome: this.environmentService.workspaceStorageHome, - localHistoryHome: this.environmentService.localHistoryHome, - userHome: this.environmentService.userHome, + appRoot: URI.file(this._environmentService.appRoot), + settingsPath: this._environmentService.machineSettingsResource, + logsPath: URI.file(this._environmentService.logsPath), + extensionsPath: URI.file(this._environmentService.extensionsPath!), + extensionHostLogsPath: URI.file(join(this._environmentService.logsPath, `exthost${RemoteAgentEnvironmentChannel._namePool++}`)), + globalStorageHome: this._environmentService.globalStorageHome, + workspaceStorageHome: this._environmentService.workspaceStorageHome, + localHistoryHome: this._environmentService.localHistoryHome, + userHome: this._environmentService.userHome, os: platform.OS, arch: process.arch, marks: performance.getMarks(), - useHostProxy: !!this.environmentService.args['use-host-proxy'] + useHostProxy: !!this._environmentService.args['use-host-proxy'] }; } private async _getTranslations(language: string): Promise { - const config = await getNLSConfiguration(language, this.environmentService.userDataPath); + const config = await getNLSConfiguration(language, this._environmentService.userDataPath); if (InternalNLSConfiguration.is(config)) { try { const content = await pfs.Promises.readFile(config._translationsConfigFile, 'utf8'); @@ -381,105 +361,43 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { private async _scanDevelopedExtensions(language: string, translations: Translations, extensionDevelopmentPaths?: string[]): Promise { if (extensionDevelopmentPaths) { - const targetPlatform = await this._extensionManagementService.getTargetPlatform(); - const extDescsP = extensionDevelopmentPaths.map(extDevPath => { - return ExtensionScanner.scanOneOrMultipleExtensions( - new ExtensionScannerInput( - this.productService.version, - this.productService.date, - this.productService.commit, - language, - true, // dev mode - extDevPath, - false, // isBuiltin - true, // isUnderDevelopment - targetPlatform, - translations // translations - ), - this.logService, - this._fileService - ); - }); - - const extDescArrays = await Promise.all(extDescsP); - let extDesc: IExtensionDescription[] = []; - for (let eds of extDescArrays) { - extDesc = extDesc.concat(eds); - } - return extDesc; + const nlsConfiguration = this.createNLSConfig({ locale: language, devMode: true, translations }); + return (await Promise.all(extensionDevelopmentPaths.map(extensionDevelopmentPath => this._extensionsScannerService.scanOneOrMultipleExtensions(URI.file(resolve(extensionDevelopmentPath)), ExtensionType.User, { nlsConfiguration })))) + .flat() + .map(e => toExtensionDescription(e, true)); } return []; } private async _scanBuiltinExtensions(language: string, translations: Translations): Promise { - const version = this.productService.version; - const commit = this.productService.commit; - const date = this.productService.date; const devMode = !!process.env['VSCODE_DEV']; - const targetPlatform = await this._extensionManagementService.getTargetPlatform(); - - const input = new ExtensionScannerInput(version, date, commit, language, devMode, getSystemExtensionsRoot(), true, false, targetPlatform, translations); - const builtinExtensions = ExtensionScanner.scanExtensions(input, this.logService, this._fileService); - let finalBuiltinExtensions: Promise = builtinExtensions; - - if (devMode) { - - class ExtraBuiltInExtensionResolver implements IExtensionResolver { - constructor(private builtInExtensions: IBuiltInExtension[]) { } - resolveExtensions(): Promise { - return Promise.resolve(this.builtInExtensions.map((ext) => { - return { name: ext.name, path: join(getExtraDevSystemExtensionsRoot(), ext.name) }; - })); - } - } - - const builtInExtensions = Promise.resolve(this.productService.builtInExtensions || []); - - const input = new ExtensionScannerInput(version, date, commit, language, devMode, getExtraDevSystemExtensionsRoot(), true, false, targetPlatform, {}); - const extraBuiltinExtensions = builtInExtensions - .then((builtInExtensions) => new ExtraBuiltInExtensionResolver(builtInExtensions)) - .then(resolver => ExtensionScanner.scanExtensions(input, this.logService, this._fileService, resolver)); - - finalBuiltinExtensions = ExtensionScanner.mergeBuiltinExtensions(builtinExtensions, extraBuiltinExtensions); - } - - return finalBuiltinExtensions; + return this._scanExtensionDescriptions(true, this.createNLSConfig({ locale: language, devMode, translations })); } private async _scanInstalledExtensions(language: string, translations: Translations): Promise { - const targetPlatform = await this._extensionManagementService.getTargetPlatform(); - const devMode = !!process.env['VSCODE_DEV']; - const input = new ExtensionScannerInput( - this.productService.version, - this.productService.date, - this.productService.commit, - language, - devMode, - this.environmentService.extensionsPath!, - false, // isBuiltin - false, // isUnderDevelopment - targetPlatform, - translations - ); - - return ExtensionScanner.scanExtensions(input, this.logService, this._fileService); + return this._scanExtensionDescriptions(false, this.createNLSConfig({ devMode: !!process.env['VSCODE_DEV'], locale: language, translations })); } private async _scanSingleExtension(extensionPath: string, isBuiltin: boolean, language: string, translations: Translations): Promise { - const targetPlatform = await this._extensionManagementService.getTargetPlatform(); - const devMode = !!process.env['VSCODE_DEV']; - const input = new ExtensionScannerInput( - this.productService.version, - this.productService.date, - this.productService.commit, - language, - devMode, - extensionPath, - isBuiltin, - false, // isUnderDevelopment - targetPlatform, - translations - ); - return ExtensionScanner.scanSingleExtension(input, this.logService, this._fileService); + const extensionLocation = URI.file(resolve(extensionPath)); + const type = isBuiltin ? ExtensionType.System : ExtensionType.User; + const nlsConfiguration = this.createNLSConfig({ devMode: !!process.env['VSCODE_DEV'], locale: platform.language, translations }); + const scannedExtension = await this._extensionsScannerService.scanExistingExtension(extensionLocation, type, { nlsConfiguration }); + return scannedExtension ? toExtensionDescription(scannedExtension, false) : null; + } + + private async _scanExtensionDescriptions(isBuiltin: boolean, nlsConfiguration: NlsConfiguration): Promise { + const scannedExtensions = isBuiltin ? await this._extensionsScannerService.scanSystemExtensions({ nlsConfiguration }) + : await this._extensionsScannerService.scanUserExtensions({ nlsConfiguration }); + return scannedExtensions.map(e => toExtensionDescription(e, false)); + } + + public createNLSConfig(input: { devMode: boolean; locale: string | undefined; translations: Translations }): NlsConfiguration { + return { + devMode: input.devMode, + locale: input.locale, + pseudo: input.locale === 'pseudo', + translations: input.translations + }; } } diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index e69a75f24ca..cf8abddae85 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -68,6 +68,7 @@ import { REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/comm import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/workbench/services/remote/common/remoteFileSystemProviderClient'; import { ExtensionHostStatusService, IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; +import { NativeExtensionsScannerService, INativeExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; const eventPrefix = 'monacoworkbench'; @@ -152,6 +153,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken const downloadChannel = socketServer.getChannel('download', router); services.set(IDownloadService, new DownloadServiceChannelClient(downloadChannel, () => getUriTransformer('renderer') /* TODO: @Sandy @Joao need dynamic context based router */)); + services.set(INativeExtensionsScannerService, new SyncDescriptor(NativeExtensionsScannerService)); services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); const instantiationService: IInstantiationService = new InstantiationService(services); @@ -176,7 +178,8 @@ export async function setupServerServices(connectionToken: ServerConnectionToken instantiationService.invokeFunction(accessor => { const extensionManagementService = accessor.get(IExtensionManagementService); - const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, extensionManagementCLIService, extensionManagementService, logService, productService, extensionHostStatusService, fileService); + const extensionsScannerService = accessor.get(INativeExtensionsScannerService); + const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, extensionManagementCLIService, logService, extensionHostStatusService, extensionsScannerService); socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel); const telemetryChannel = new ServerTelemetryChannel(accessor.get(IServerTelemetryService), appInsightsAppender); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 2de5c8e1411..31f4499b9de 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -2348,6 +2348,11 @@ export class ExtensionStatusAction extends ExtensionAction { } } + if (isEnabled && !isRunning && !this.extension.local.isValid) { + const errors = this.extension.local.validations.filter(v => v.severity === Severity.Error).map(v => v.message); + this.updateStatus({ icon: errorIcon, message: new MarkdownString(errors.join(' ').trim()) }, true); + } + } private updateStatus(status: ExtensionStatus | undefined, updateClass: boolean): void { diff --git a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts index c88dede88ba..089434c2a84 100644 --- a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts @@ -58,6 +58,8 @@ export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScanne readmeUrl: e.readmePath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.readmePath) : undefined, changelogUrl: e.changelogPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.changelogPath) : undefined, targetPlatform: TargetPlatform.WEB, + validations: [], + isValid: true })); } } diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index 92ec5bd6f8d..ace28ce128f 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -36,8 +36,9 @@ import { IExtensionStorageService } from 'vs/platform/extensionManagement/common import { isNonEmptyArray } from 'vs/base/common/arrays'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { ExtensionManifestValidator } from 'vs/workbench/services/extensions/common/extensionPoints'; import { IProductService } from 'vs/platform/product/common/productService'; +import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; +import Severity from 'vs/base/common/severity'; type GalleryExtensionInfo = { readonly id: string; preRelease?: boolean; migrateStorageFrom?: string }; type ExtensionInfo = { readonly id: string; preRelease: boolean }; @@ -196,8 +197,6 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const extension = await this.toScannedExtension(webExtension, true); if (extension.isValid || !scanOptions?.skipInvalidExtensions) { result.push(extension); - } else { - this.logService.info(`Ignoring additional builtin extension ${webExtension.identifier.id} because it is not valid.`, extension.validationMessages); } } catch (error) { this.logService.info(`Error while fetching the additional builtin extension ${location.toString()}.`, getErrorMessage(error)); @@ -225,8 +224,6 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const extension = await this.toScannedExtension(webExtension, true); if (extension.isValid || !scanOptions?.skipInvalidExtensions) { result.push(extension); - } else { - this.logService.info(`Ignoring additional builtin extension ${webExtension.identifier.id} because it is not valid.`, extension.validationMessages); } } catch (error) { this.logService.info(`Ignoring additional builtin extension ${webExtension.identifier.id} because there is an error while converting it into scanned extension`, getErrorMessage(error)); @@ -497,8 +494,6 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const extension = await this.toScannedExtension(webExtension, false); if (extension.isValid || !scanOptions?.skipInvalidExtensions) { result.set(extension.identifier.id.toLowerCase(), extension); - } else { - this.logService.info(`Skipping user installed extension ${webExtension.identifier.id} because it is not valid.`, extension.validationMessages); } } catch (error) { if (scanOptions?.bailOut) { @@ -580,8 +575,14 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const uuid = (webExtension.metadata)?.id; - const validationMessages: string[] = []; - const isValid = ExtensionManifestValidator.isValidExtensionManifest(this.productService.version, this.productService.date, webExtension.location, manifest, false, validationMessages); + const validations = validateExtensionManifest(this.productService.version, this.productService.date, webExtension.location, manifest, false); + let isValid = true; + for (const validation of validations) { + if (validation.severity === Severity.Error) { + isValid = false; + this.logService.error(validation.message); + } + } return { identifier: { id: webExtension.identifier.id, uuid: webExtension.identifier.uuid || uuid }, @@ -593,7 +594,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten changelogUrl: webExtension.changelogUri, metadata: webExtension.metadata, targetPlatform: TargetPlatform.WEB, - validationMessages, + validations, isValid }; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index efb907e741c..5cc547527c1 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -140,8 +140,6 @@ export interface IWorkbenchExtensionEnablementService { export interface IScannedExtension extends IExtension { readonly metadata?: Metadata; - readonly isValid: boolean; - readonly validationMessages: readonly string[]; } export type ScanOptions = { readonly bailOut?: boolean; readonly skipInvalidExtensions?: boolean }; diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 86784926919..0334f7f0b99 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -32,7 +32,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; -import { ILog } from 'vs/workbench/services/extensions/common/extensionPoints'; import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { ApiProposalName, allApiProposals } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { forEach } from 'vs/base/common/collections'; @@ -1245,26 +1244,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx //#region Called by extension host - protected _createLogger(): ILog { - return { - error: (message: string | Error): void => { - if (this._isDev) { - this._notificationService.notify({ severity: Severity.Error, message }); - } - this._logService.error(message); - }, - warn: (message: string): void => { - if (this._isDev) { - this._notificationService.notify({ severity: Severity.Warning, message }); - } - this._logService.warn(message); - }, - info: (message: string): void => { - this._logService.info(message); - } - }; - } - private _acquireInternalAPI(): IInternalExtensionService { return { _activateById: (extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise => { @@ -1329,7 +1308,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } protected async _scanWebExtensions(): Promise { - const log = this._createLogger(); const system: IExtensionDescription[] = [], user: IExtensionDescription[] = [], development: IExtensionDescription[] = []; try { await Promise.all([ @@ -1338,9 +1316,9 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._webExtensionsScannerService.scanExtensionsUnderDevelopment().then(extensions => development.push(...extensions.map(e => toExtensionDescription(e, true)))) ]); } catch (error) { - log.error(error); + this._logService.error(error); } - return dedupExtensions(system, user, development, log); + return dedupExtensions(system, user, development, this._logService); } //#endregion diff --git a/src/vs/workbench/services/extensions/common/extensionPoints.ts b/src/vs/workbench/services/extensions/common/extensionPoints.ts deleted file mode 100644 index 892afa14c54..00000000000 --- a/src/vs/workbench/services/extensions/common/extensionPoints.ts +++ /dev/null @@ -1,750 +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 nls from 'vs/nls'; -import * as path from 'vs/base/common/path'; -import * as resources from 'vs/base/common/resources'; -import * as semver from 'vs/base/common/semver/semver'; -import * as json from 'vs/base/common/json'; -import * as arrays from 'vs/base/common/arrays'; -import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; -import * as types from 'vs/base/common/types'; -import { URI } from 'vs/base/common/uri'; -import { getGalleryExtensionId, getExtensionId, ExtensionKey } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { isValidExtensionVersion } from 'vs/platform/extensions/common/extensionValidator'; -import { ExtensionIdentifier, IExtensionDescription, IExtensionManifest, IRelaxedExtensionDescription, TargetPlatform, UNDEFINED_PUBLISHER } from 'vs/platform/extensions/common/extensions'; -import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; - -const MANIFEST_FILE = 'package.json'; - -export interface Translations { - [id: string]: string; -} - -export namespace Translations { - export function equals(a: Translations, b: Translations): boolean { - if (a === b) { - return true; - } - let aKeys = Object.keys(a); - let bKeys: Set = new Set(); - for (let key of Object.keys(b)) { - bKeys.add(key); - } - if (aKeys.length !== bKeys.size) { - return false; - } - - for (let key of aKeys) { - if (a[key] !== b[key]) { - return false; - } - bKeys.delete(key); - } - return bKeys.size === 0; - } -} - -export interface ILog { - error(message: string | Error): void; - warn(message: string): void; - info(message: string): void; -} - -export interface NlsConfiguration { - readonly devMode: boolean; - readonly locale: string | undefined; - readonly pseudo: boolean; - readonly translations: Translations; -} - -abstract class ExtensionManifestHandler { - - protected readonly _absoluteManifestPath: string; - - constructor( - protected readonly _ourVersion: string, - protected readonly _ourProductDate: string | undefined, - protected readonly _absoluteFolderPath: string, - protected readonly _isBuiltin: boolean, - protected readonly _isUnderDevelopment: boolean, - protected readonly _log: ILog, - protected readonly _fileService: IFileService - ) { - this._absoluteManifestPath = path.join(this._absoluteFolderPath, MANIFEST_FILE); - } - - protected _error(source: string, message: string): void { - this._log.error(`[${source}]: ${message}`); - } - - protected _warn(source: string, message: string): void { - this._log.warn(`[${source}]: ${message}`); - } - -} - -class ExtensionManifestParser extends ExtensionManifestHandler { - - private static _fastParseJSON(text: string, errors: json.ParseError[]): T { - try { - return JSON.parse(text); - } catch (err) { - // invalid JSON, let's get good errors - return json.parse(text, errors); - } - } - - public parse(): Promise { - return readFile(this._fileService, this._absoluteManifestPath).then((manifestContents) => { - const errors: json.ParseError[] = []; - const manifest = ExtensionManifestParser._fastParseJSON(manifestContents, errors); - if (json.getNodeType(manifest) !== 'object') { - this._error(this._absoluteFolderPath, nls.localize('jsonParseInvalidType', "Invalid manifest file {0}: Not an JSON object.", this._absoluteManifestPath)); - } else if (errors.length === 0) { - manifest.uuid = manifest.__metadata?.id; - manifest.targetPlatform = manifest.__metadata?.targetPlatform ?? TargetPlatform.UNDEFINED; - manifest.isUserBuiltin = !!manifest.__metadata?.isBuiltin; - delete manifest.__metadata; - return manifest; - } else { - errors.forEach(e => { - this._error(this._absoluteFolderPath, nls.localize('jsonParseFail', "Failed to parse {0}: [{1}, {2}] {3}.", this._absoluteManifestPath, e.offset, e.length, getParseErrorMessage(e.error))); - }); - } - return null; - }, (err) => { - if (err.code === 'ENOENT') { - return null; - } - - this._error(this._absoluteFolderPath, nls.localize('fileReadFail', "Cannot read file {0}: {1}.", this._absoluteManifestPath, err.message)); - return null; - }); - } -} - -interface MessageBag { - [key: string]: string | { message: string; comment: string[] }; -} - -interface TranslationBundle { - contents: { - package: MessageBag; - }; -} - -interface LocalizedMessages { - values: MessageBag | undefined; - default: string | null; -} - -class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { - - private readonly _nlsConfig: NlsConfiguration; - - constructor( - ourVersion: string, - ourProductDate: string | undefined, - absoluteFolderPath: string, - isBuiltin: boolean, - isUnderDevelopment: boolean, - nlsConfig: NlsConfiguration, - log: ILog, - fileService: IFileService - ) { - super(ourVersion, ourProductDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, log, fileService); - this._nlsConfig = nlsConfig; - } - - public replaceNLS(extensionDescription: IExtensionDescription): Promise { - const reportErrors = (localized: string | null, errors: json.ParseError[]): void => { - errors.forEach((error) => { - this._error(this._absoluteFolderPath, nls.localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localized, getParseErrorMessage(error.error))); - }); - }; - const reportInvalidFormat = (localized: string | null): void => { - this._error(this._absoluteFolderPath, nls.localize('jsonInvalidFormat', "Invalid format {0}: JSON object expected.", localized)); - }; - - let extension = path.extname(this._absoluteManifestPath); - let basename = this._absoluteManifestPath.substr(0, this._absoluteManifestPath.length - extension.length); - - const translationId = `${extensionDescription.publisher}.${extensionDescription.name}`; - let translationPath = this._nlsConfig.translations[translationId]; - let localizedMessages: Promise; - if (translationPath) { - localizedMessages = readFile(this._fileService, translationPath).then((content) => { - let errors: json.ParseError[] = []; - let translationBundle: TranslationBundle = json.parse(content, errors); - if (errors.length > 0) { - reportErrors(translationPath, errors); - return { values: undefined, default: `${basename}.nls.json` }; - } else if (json.getNodeType(translationBundle) !== 'object') { - reportInvalidFormat(translationPath); - return { values: undefined, default: `${basename}.nls.json` }; - } else { - let values = translationBundle.contents ? translationBundle.contents.package : undefined; - return { values: values, default: `${basename}.nls.json` }; - } - }, (error) => { - return { values: undefined, default: `${basename}.nls.json` }; - }); - } else { - localizedMessages = existsFile(this._fileService, basename + '.nls' + extension).then(exists => { - if (!exists) { - return undefined; - } - return ExtensionManifestNLSReplacer.findMessageBundles(this._nlsConfig, basename, this._fileService).then((messageBundle) => { - if (!messageBundle.localized) { - return { values: undefined, default: messageBundle.original }; - } - return readFile(this._fileService, messageBundle.localized).then(messageBundleContent => { - let errors: json.ParseError[] = []; - let messages: MessageBag = json.parse(messageBundleContent, errors); - if (errors.length > 0) { - reportErrors(messageBundle.localized, errors); - return { values: undefined, default: messageBundle.original }; - } else if (json.getNodeType(messages) !== 'object') { - reportInvalidFormat(messageBundle.localized); - return { values: undefined, default: messageBundle.original }; - } - return { values: messages, default: messageBundle.original }; - }, (err) => { - return { values: undefined, default: messageBundle.original }; - }); - }, (err) => { - return undefined; - }); - }); - } - - return localizedMessages.then((localizedMessages) => { - if (localizedMessages === undefined) { - return extensionDescription; - } - let errors: json.ParseError[] = []; - // resolveOriginalMessageBundle returns null if localizedMessages.default === undefined; - return this.resolveOriginalMessageBundle(localizedMessages.default, errors).then((defaults) => { - if (errors.length > 0) { - reportErrors(localizedMessages.default, errors); - return extensionDescription; - } else if (json.getNodeType(localizedMessages) !== 'object') { - reportInvalidFormat(localizedMessages.default); - return extensionDescription; - } - const localized = localizedMessages.values || Object.create(null); - ExtensionManifestNLSReplacer._replaceNLStrings(this._nlsConfig, extensionDescription, localized, defaults, this._absoluteFolderPath, this._log); - return extensionDescription; - }); - }, (err) => { - return extensionDescription; - }); - } - - /** - * Parses original message bundle, returns null if the original message bundle is null. - */ - private resolveOriginalMessageBundle(originalMessageBundle: string | null, errors: json.ParseError[]) { - return new Promise<{ [key: string]: string } | null>((c, e) => { - if (originalMessageBundle) { - readFile(this._fileService, originalMessageBundle).then(originalBundleContent => { - c(json.parse(originalBundleContent, errors)); - }, (err) => { - c(null); - }); - } else { - c(null); - } - }); - } - - /** - * Finds localized message bundle and the original (unlocalized) one. - * If the localized file is not present, returns null for the original and marks original as localized. - */ - private static findMessageBundles(nlsConfig: NlsConfiguration, basename: string, fileService: IFileService): Promise<{ localized: string; original: string | null }> { - return new Promise<{ localized: string; original: string | null }>((c, e) => { - function loop(basename: string, locale: string): void { - let toCheck = `${basename}.nls.${locale}.json`; - existsFile(fileService, toCheck).then(exists => { - if (exists) { - c({ localized: toCheck, original: `${basename}.nls.json` }); - } - let index = locale.lastIndexOf('-'); - if (index === -1) { - c({ localized: `${basename}.nls.json`, original: null }); - } else { - locale = locale.substring(0, index); - loop(basename, locale); - } - }); - } - - if (nlsConfig.devMode || nlsConfig.pseudo || !nlsConfig.locale) { - return c({ localized: basename + '.nls.json', original: null }); - } - loop(basename, nlsConfig.locale); - }); - } - - /** - * This routine makes the following assumptions: - * The root element is an object literal - */ - private static _replaceNLStrings(nlsConfig: NlsConfiguration, literal: T, messages: MessageBag, originalMessages: MessageBag | null, messageScope: string, log: ILog): void { - function processEntry(obj: any, key: string | number, command?: boolean) { - const value = obj[key]; - if (types.isString(value)) { - const str = value; - const length = str.length; - if (length > 1 && str[0] === '%' && str[length - 1] === '%') { - const messageKey = str.substr(1, length - 2); - let translated = messages[messageKey]; - // If the messages come from a language pack they might miss some keys - // Fill them from the original messages. - if (translated === undefined && originalMessages) { - translated = originalMessages[messageKey]; - } - let message: string | undefined = typeof translated === 'string' ? translated : (typeof translated?.message === 'string' ? translated.message : undefined); - if (message !== undefined) { - if (nlsConfig.pseudo) { - // FF3B and FF3D is the Unicode zenkaku representation for [ and ] - message = '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D'; - } - obj[key] = command && (key === 'title' || key === 'category') && originalMessages ? { value: message, original: originalMessages[messageKey] } : message; - } else { - const message = nls.localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey); - log.warn(`[${messageScope}]: ${message}`); - } - } - } else if (types.isObject(value)) { - for (let k in value) { - if (value.hasOwnProperty(k)) { - k === 'commands' ? processEntry(value, k, true) : processEntry(value, k, command); - } - } - } else if (types.isArray(value)) { - for (let i = 0; i < value.length; i++) { - processEntry(value, i, command); - } - } - } - - for (let key in literal) { - if (literal.hasOwnProperty(key)) { - processEntry(literal, key); - } - } - } -} - -export class ExtensionManifestValidator extends ExtensionManifestHandler { - validate(_extensionDescription: IExtensionDescription): IExtensionDescription | null { - let extensionDescription = _extensionDescription; - extensionDescription.isBuiltin = this._isBuiltin; - extensionDescription.isUserBuiltin = !this._isBuiltin && !!extensionDescription.isUserBuiltin; - extensionDescription.isUnderDevelopment = this._isUnderDevelopment; - - let notices: string[] = []; - if (!ExtensionManifestValidator.isValidExtensionManifest(this._ourVersion, this._ourProductDate, URI.file(this._absoluteFolderPath), extensionDescription, extensionDescription.isBuiltin, notices)) { - notices.forEach((error) => { - this._error(this._absoluteFolderPath, error); - }); - return null; - } - - // in this case the notices are warnings - notices.forEach((error) => { - this._warn(this._absoluteFolderPath, error); - }); - - // allow publisher to be undefined to make the initial extension authoring experience smoother - if (!extensionDescription.publisher) { - extensionDescription.publisher = UNDEFINED_PUBLISHER; - } - - // id := `publisher.name` - extensionDescription.id = getExtensionId(extensionDescription.publisher, extensionDescription.name); - extensionDescription.identifier = new ExtensionIdentifier(extensionDescription.id); - - extensionDescription.extensionLocation = URI.file(this._absoluteFolderPath); - - return extensionDescription; - } - - public static isValidExtensionManifest(productVersion: string, productDate: string | undefined, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, notices: string[]): boolean { - - if (!ExtensionManifestValidator.baseIsValidExtensionManifest(extensionLocation, extensionManifest, notices)) { - return false; - } - - if (!semver.valid(extensionManifest.version)) { - notices.push(nls.localize('notSemver', "Extension version is not semver compatible.")); - return false; - } - - return isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices); - } - - private static baseIsValidExtensionManifest(extensionLocation: URI, extensionDescription: IExtensionManifest, notices: string[]): boolean { - if (!extensionDescription) { - notices.push(nls.localize('extensionDescription.empty', "Got empty extension description")); - return false; - } - if (typeof extensionDescription.publisher !== 'undefined' && typeof extensionDescription.publisher !== 'string') { - notices.push(nls.localize('extensionDescription.publisher', "property publisher must be of type `string`.")); - return false; - } - if (typeof extensionDescription.name !== 'string') { - notices.push(nls.localize('extensionDescription.name', "property `{0}` is mandatory and must be of type `string`", 'name')); - return false; - } - if (typeof extensionDescription.version !== 'string') { - notices.push(nls.localize('extensionDescription.version', "property `{0}` is mandatory and must be of type `string`", 'version')); - return false; - } - if (!extensionDescription.engines) { - notices.push(nls.localize('extensionDescription.engines', "property `{0}` is mandatory and must be of type `object`", 'engines')); - return false; - } - if (typeof extensionDescription.engines.vscode !== 'string') { - notices.push(nls.localize('extensionDescription.engines.vscode', "property `{0}` is mandatory and must be of type `string`", 'engines.vscode')); - return false; - } - if (typeof extensionDescription.extensionDependencies !== 'undefined') { - if (!ExtensionManifestValidator._isStringArray(extensionDescription.extensionDependencies)) { - notices.push(nls.localize('extensionDescription.extensionDependencies', "property `{0}` can be omitted or must be of type `string[]`", 'extensionDependencies')); - return false; - } - } - if (typeof extensionDescription.activationEvents !== 'undefined') { - if (!ExtensionManifestValidator._isStringArray(extensionDescription.activationEvents)) { - notices.push(nls.localize('extensionDescription.activationEvents1', "property `{0}` can be omitted or must be of type `string[]`", 'activationEvents')); - return false; - } - if (typeof extensionDescription.main === 'undefined' && typeof extensionDescription.browser === 'undefined') { - notices.push(nls.localize('extensionDescription.activationEvents2', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main')); - return false; - } - } - if (typeof extensionDescription.extensionKind !== 'undefined') { - if (typeof extensionDescription.main === 'undefined') { - notices.push(nls.localize('extensionDescription.extensionKind', "property `{0}` can be defined only if property `main` is also defined.", 'extensionKind')); - // not a failure case - } - } - if (typeof extensionDescription.main !== 'undefined') { - if (typeof extensionDescription.main !== 'string') { - notices.push(nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main')); - return false; - } else { - const mainLocation = resources.joinPath(extensionLocation, extensionDescription.main); - if (!resources.isEqualOrParent(mainLocation, extensionLocation)) { - notices.push(nls.localize('extensionDescription.main2', "Expected `main` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", mainLocation.path, extensionLocation.path)); - // not a failure case - } - } - if (typeof extensionDescription.activationEvents === 'undefined') { - notices.push(nls.localize('extensionDescription.main3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main')); - return false; - } - } - if (typeof extensionDescription.browser !== 'undefined') { - if (typeof extensionDescription.browser !== 'string') { - notices.push(nls.localize('extensionDescription.browser1', "property `{0}` can be omitted or must be of type `string`", 'browser')); - return false; - } else { - const browserLocation = resources.joinPath(extensionLocation, extensionDescription.browser); - if (!resources.isEqualOrParent(browserLocation, extensionLocation)) { - notices.push(nls.localize('extensionDescription.browser2', "Expected `browser` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", browserLocation.path, extensionLocation.path)); - // not a failure case - } - } - if (typeof extensionDescription.activationEvents === 'undefined') { - notices.push(nls.localize('extensionDescription.browser3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'browser')); - return false; - } - } - return true; - } - - private static _isStringArray(arr: string[]): boolean { - if (!Array.isArray(arr)) { - return false; - } - for (let i = 0, len = arr.length; i < len; i++) { - if (typeof arr[i] !== 'string') { - return false; - } - } - return true; - } -} - -export class ExtensionScannerInput { - - public mtime: number | undefined; - - constructor( - public readonly ourVersion: string, - public readonly ourProductDate: string | undefined, - public readonly commit: string | undefined, - public readonly locale: string | undefined, - public readonly devMode: boolean, - public readonly absoluteFolderPath: string, - public readonly isBuiltin: boolean, - public readonly isUnderDevelopment: boolean, - public readonly targetPlatform: TargetPlatform, - public readonly translations: Translations - ) { - // Keep empty!! (JSON.parse) - } - - public static createNLSConfig(input: ExtensionScannerInput): NlsConfiguration { - return { - devMode: input.devMode, - locale: input.locale, - pseudo: input.locale === 'pseudo', - translations: input.translations - }; - } - - public static equals(a: ExtensionScannerInput, b: ExtensionScannerInput): boolean { - return ( - a.ourVersion === b.ourVersion - && a.ourProductDate === b.ourProductDate - && a.commit === b.commit - && a.locale === b.locale - && a.devMode === b.devMode - && a.absoluteFolderPath === b.absoluteFolderPath - && a.isBuiltin === b.isBuiltin - && a.isUnderDevelopment === b.isUnderDevelopment - && a.mtime === b.mtime - && a.targetPlatform === b.targetPlatform - && Translations.equals(a.translations, b.translations) - ); - } -} - -export interface IExtensionReference { - name: string; - path: string; -} - -export interface IExtensionResolver { - resolveExtensions(): Promise; -} - -class DefaultExtensionResolver implements IExtensionResolver { - - constructor( - private readonly root: string, - private readonly _fileService: IFileService - ) { - } - - resolveExtensions(): Promise { - return readDirsInDir(this._fileService, this.root) - .then(folders => folders.map(name => ({ name, path: path.join(this.root, name) }))); - } -} - -export class ExtensionScanner { - - /** - * Read the extension defined in `absoluteFolderPath` - */ - private static scanExtension(version: string, productDate: string | undefined, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration, log: ILog, fileService: IFileService): Promise { - absoluteFolderPath = path.normalize(absoluteFolderPath); - - let parser = new ExtensionManifestParser(version, productDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, log, fileService); - return parser.parse().then((extensionDescription) => { - if (extensionDescription === null) { - return null; - } - - let nlsReplacer = new ExtensionManifestNLSReplacer(version, productDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig, log, fileService); - return nlsReplacer.replaceNLS(extensionDescription); - }).then((extensionDescription) => { - if (extensionDescription === null) { - return null; - } - - let validator = new ExtensionManifestValidator(version, productDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, log, fileService); - return validator.validate(extensionDescription); - }); - } - - /** - * Scan a list of extensions defined in `absoluteFolderPath` - */ - public static async scanExtensions(input: ExtensionScannerInput, log: ILog, fileService: IFileService, resolver: IExtensionResolver | null = null): Promise { - const absoluteFolderPath = input.absoluteFolderPath; - const isBuiltin = input.isBuiltin; - const isUnderDevelopment = input.isUnderDevelopment; - - if (!resolver) { - resolver = new DefaultExtensionResolver(absoluteFolderPath, fileService); - } - - try { - let obsolete: { [folderName: string]: boolean } = {}; - if (!isBuiltin) { - try { - const obsoleteFileContents = await readFile(fileService, path.join(absoluteFolderPath, '.obsolete')); - obsolete = JSON.parse(obsoleteFileContents); - } catch (err) { - // Don't care - } - } - - let refs = await resolver.resolveExtensions(); - - // Ensure the same extension order - refs.sort((a, b) => a.name < b.name ? -1 : 1); - - if (!isBuiltin) { - refs = refs.filter(ref => ref.name.indexOf('.') !== 0); // Do not consider user extension folder starting with `.` - } - - const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - let _extensionDescriptions = await Promise.all(refs.map(r => this.scanExtension(input.ourVersion, input.ourProductDate, r.path, isBuiltin, isUnderDevelopment, nlsConfig, log, fileService))); - let extensionDescriptions = arrays.coalesce(_extensionDescriptions); - extensionDescriptions = extensionDescriptions.filter(item => item !== null && !obsolete[new ExtensionKey({ id: getGalleryExtensionId(item.publisher, item.name) }, item.version, item.targetPlatform).toString()]); - - if (!isBuiltin) { - extensionDescriptions = this.filterOutdatedExtensions(extensionDescriptions, input.targetPlatform); - } - - extensionDescriptions.sort((a, b) => { - if (a.extensionLocation.fsPath < b.extensionLocation.fsPath) { - return -1; - } - return 1; - }); - return extensionDescriptions; - } catch (err) { - log.error(`Error scanning extensions at ${absoluteFolderPath}:`); - log.error(err); - return []; - } - } - - /** - * Combination of scanExtension and scanExtensions: If an extension manifest is found at root, we load just this extension, - * otherwise we assume the folder contains multiple extensions. - */ - public static scanOneOrMultipleExtensions(input: ExtensionScannerInput, log: ILog, fileService: IFileService): Promise { - const absoluteFolderPath = input.absoluteFolderPath; - const isBuiltin = input.isBuiltin; - const isUnderDevelopment = input.isUnderDevelopment; - - return existsFile(fileService, path.join(absoluteFolderPath, MANIFEST_FILE)).then((exists) => { - if (exists) { - const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - return this.scanExtension(input.ourVersion, input.ourProductDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig, log, fileService).then((extensionDescription) => { - if (extensionDescription === null) { - return []; - } - return [extensionDescription]; - }); - } - return this.scanExtensions(input, log, fileService); - }, (err) => { - log.error(`Error scanning extensions at ${absoluteFolderPath}:`); - log.error(err); - return []; - }); - } - - public static scanSingleExtension(input: ExtensionScannerInput, log: ILog, fileService: IFileService): Promise { - const absoluteFolderPath = input.absoluteFolderPath; - const isBuiltin = input.isBuiltin; - const isUnderDevelopment = input.isUnderDevelopment; - const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - return this.scanExtension(input.ourVersion, input.ourProductDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig, log, fileService); - } - - public static mergeBuiltinExtensions(builtinExtensions: Promise, extraBuiltinExtensions: Promise): Promise { - return Promise.all([builtinExtensions, extraBuiltinExtensions]).then(([builtinExtensions, extraBuiltinExtensions]) => { - let resultMap: { [id: string]: IExtensionDescription } = Object.create(null); - for (let i = 0, len = builtinExtensions.length; i < len; i++) { - resultMap[ExtensionIdentifier.toKey(builtinExtensions[i].identifier)] = builtinExtensions[i]; - } - // Overwrite with extensions found in extra - for (let i = 0, len = extraBuiltinExtensions.length; i < len; i++) { - resultMap[ExtensionIdentifier.toKey(extraBuiltinExtensions[i].identifier)] = extraBuiltinExtensions[i]; - } - - let resultArr = Object.keys(resultMap).map((id) => resultMap[id]); - resultArr.sort((a, b) => { - const aLastSegment = path.basename(a.extensionLocation.fsPath); - const bLastSegment = path.basename(b.extensionLocation.fsPath); - if (aLastSegment < bLastSegment) { - return -1; - } - if (aLastSegment > bLastSegment) { - return 1; - } - return 0; - }); - return resultArr; - }); - } - - private static filterOutdatedExtensions(extensions: IExtensionDescription[], targetPlatform: TargetPlatform): IExtensionDescription[] { - const result = new Map(); - for (const extension of extensions) { - const extensionKey = extension.identifier.value; - const existing = result.get(extensionKey); - if (existing) { - if (semver.gt(existing.version, extension.version)) { - continue; - } - if (semver.eq(existing.version, extension.version) && existing.targetPlatform === targetPlatform) { - continue; - } - } - result.set(extensionKey, extension); - } - return [...result.values()]; - } -} - -async function readFile(fileService: IFileService, filename: string): Promise { - try { - const contents = await fileService.readFile(URI.file(filename), { atomic: true }); - return contents.value.toString(); - } catch (err) { - if (toFileOperationResult(err) === FileOperationResult.FILE_NOT_FOUND) { - const nodeLikeError = new Error(`File not found`); - (nodeLikeError).code = 'ENOENT'; - throw nodeLikeError; - } - throw err; - } -} - -async function existsFile(fileService: IFileService, filename: string): Promise { - try { - const stat = await fileService.resolve(URI.file(filename)); - return stat.isFile; - } catch (err) { - return false; - } -} - -async function readDirsInDir(fileService: IFileService, dirPath: string): Promise { - const stat = await fileService.resolve(URI.file(dirPath)); - const result: string[] = []; - for (const child of (stat.children || [])) { - if (child.isDirectory) { - result.push(child.name); - } - } - return result; -} diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 5ab3abcb921..db9df68c5f6 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -377,6 +377,8 @@ export function toExtension(extensionDescription: IExtensionDescription): IExten manifest: extensionDescription, location: extensionDescription.extensionLocation, targetPlatform: extensionDescription.targetPlatform, + validations: [], + isValid: true }; } diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index 4050fa6a460..977052ebcfc 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ILog } from 'vs/workbench/services/extensions/common/extensionPoints'; import { localize } from 'vs/nls'; +import { ILogService } from 'vs/platform/log/common/log'; -export function dedupExtensions(system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[], log: ILog): IExtensionDescription[] { +export function dedupExtensions(system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[], logService: ILogService): IExtensionDescription[] { let result = new Map(); system.forEach((systemExtension) => { const extensionKey = ExtensionIdentifier.toKey(systemExtension.identifier); const extension = result.get(extensionKey); if (extension) { - log.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, systemExtension.extensionLocation.fsPath)); + logService.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, systemExtension.extensionLocation.fsPath)); } result.set(extensionKey, systemExtension); }); @@ -25,13 +25,13 @@ export function dedupExtensions(system: IExtensionDescription[], user: IExtensio // Overwriting a builtin extension inherits the `isBuiltin` property and it doesn't show a warning (userExtension).isBuiltin = true; } else { - log.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, userExtension.extensionLocation.fsPath)); + logService.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, userExtension.extensionLocation.fsPath)); } } result.set(extensionKey, userExtension); }); development.forEach(developedExtension => { - log.info(localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionLocation.fsPath)); + logService.info(localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionLocation.fsPath)); const extensionKey = ExtensionIdentifier.toKey(developedExtension.identifier); const extension = result.get(extensionKey); if (extension) { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index cfa6569db24..606ee940774 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -145,7 +145,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten return this._remoteAgentService.scanSingleExtension(extension.location, extension.type === ExtensionType.System); } - return this._extensionScanner.scanSingleExtension(extension.location.fsPath, extension.type === ExtensionType.System, this._createLogger()); + return this._extensionScanner.scanSingleExtension(extension.location.fsPath, extension.type === ExtensionType.System); } private async _scanAllLocalExtensions(): Promise { @@ -431,7 +431,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten } protected async _scanAndHandleExtensions(): Promise { - this._extensionScanner.startScanningExtensions(this._createLogger()); + this._extensionScanner.startScanningExtensions(); const remoteAuthority = this._environmentService.remoteAuthority; diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index c487b74c2b5..47403cf5f56 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -6,41 +6,66 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import * as errors from 'vs/base/common/errors'; -import { FileAccess, Schemas } from 'vs/base/common/network'; import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; -import { joinPath, originalFSPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; -import { BUILTIN_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER, USER_MANIFEST_CACHE_FILE, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { BUILTIN_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER, USER_MANIFEST_CACHE_FILE, IExtensionDescription, IRelaxedExtensionDescription, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { Translations, ILog, ExtensionScanner, ExtensionScannerInput, IExtensionReference, IExtensionResolver } from 'vs/workbench/services/extensions/common/extensionPoints'; import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { IFileService } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { INativeExtensionsScannerService, IScannedExtension, NlsConfiguration, toExtensionDescription, Translations } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { ILogService } from 'vs/platform/log/common/log'; interface IExtensionCacheData { input: ExtensionScannerInput; result: IExtensionDescription[]; } -let _SystemExtensionsRoot: string | null = null; -function getSystemExtensionsRoot(): string { - if (!_SystemExtensionsRoot) { - _SystemExtensionsRoot = path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions')); +class ExtensionScannerInput { + + public mtime: number | undefined; + + constructor( + public readonly ourVersion: string, + public readonly ourProductDate: string | undefined, + public readonly commit: string | undefined, + public readonly locale: string | undefined, + public readonly devMode: boolean, + public readonly absoluteFolderPath: string, + public readonly isBuiltin: boolean, + public readonly isUnderDevelopment: boolean, + public readonly translations: Translations + ) { + // Keep empty!! (JSON.parse) } - return _SystemExtensionsRoot; -} -let _ExtraDevSystemExtensionsRoot: string | null = null; -function getExtraDevSystemExtensionsRoot(): string { - if (!_ExtraDevSystemExtensionsRoot) { - _ExtraDevSystemExtensionsRoot = path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions')); + public static createNLSConfig(input: { devMode: boolean; locale: string | undefined; translations: Translations }): NlsConfiguration { + return { + devMode: input.devMode, + locale: input.locale, + pseudo: input.locale === 'pseudo', + translations: input.translations + }; + } + + public static equals(a: ExtensionScannerInput, b: ExtensionScannerInput): boolean { + return ( + a.ourVersion === b.ourVersion + && a.ourProductDate === b.ourProductDate + && a.commit === b.commit + && a.locale === b.locale + && a.devMode === b.devMode + && a.absoluteFolderPath === b.absoluteFolderPath + && a.isBuiltin === b.isBuiltin + && a.isUnderDevelopment === b.isUnderDevelopment + && a.mtime === b.mtime + && Translations.equals(a.translations, b.translations) + ); } - return _ExtraDevSystemExtensionsRoot; } export class CachedExtensionScanner { @@ -56,7 +81,8 @@ export class CachedExtensionScanner { @IHostService private readonly _hostService: IHostService, @IProductService private readonly _productService: IProductService, @IFileService private readonly _fileService: IFileService, - @IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService + @INativeExtensionsScannerService private readonly _extensionsScannerService: INativeExtensionsScannerService, + @ILogService private readonly _logService: ILogService, ) { this.scannedExtensions = new Promise((resolve, reject) => { this._scannedExtensionsResolve = resolve; @@ -65,24 +91,20 @@ export class CachedExtensionScanner { this.translationConfig = this._readTranslationConfig(); } - public async scanSingleExtension(path: string, isBuiltin: boolean, log: ILog): Promise { + public async scanSingleExtension(extensionPath: string, isBuiltin: boolean): Promise { const translations = await this.translationConfig; - - const version = this._productService.version; - const commit = this._productService.commit; - const date = this._productService.date; - const devMode = !this._environmentService.isBuilt; - const locale = platform.language; - const targetPlatform = await this._extensionManagementService.getTargetPlatform(); - const input = new ExtensionScannerInput(version, date, commit, locale, devMode, path, isBuiltin, false, targetPlatform, translations); - return ExtensionScanner.scanSingleExtension(input, log, this._fileService); + const extensionLocation = URI.file(path.resolve(extensionPath)); + const type = isBuiltin ? ExtensionType.System : ExtensionType.User; + const nlsConfiguration = ExtensionScannerInput.createNLSConfig({ devMode: !this._environmentService.isBuilt, locale: platform.language, translations }); + const scannedExtension = await this._extensionsScannerService.scanExistingExtension(extensionLocation, type, { nlsConfiguration }); + return scannedExtension ? toExtensionDescription(scannedExtension, false) : null; } - public async startScanningExtensions(log: ILog): Promise { + public async startScanningExtensions(): Promise { try { const translations = await this.translationConfig; - const { system, user, development } = await this._scanInstalledExtensions(log, translations); - const r = dedupExtensions(system, user, development, log); + const { system, user, development } = await this._scanInstalledExtensions(translations); + const r = dedupExtensions(system, user, development, this._logService); this._scannedExtensionsResolve(r); } catch (err) { this._scannedExtensionsReject(err); @@ -93,7 +115,7 @@ export class CachedExtensionScanner { const cacheFolder = path.join(this._environmentService.userDataPath, MANIFEST_CACHE_FOLDER); const cacheFile = path.join(cacheFolder, cacheKey); - const expected = JSON.parse(JSON.stringify(await ExtensionScanner.scanExtensions(input, new NullLogger(), this._fileService))); + const expected = JSON.parse(JSON.stringify(await this.scanExtensionDescriptions(input.isBuiltin, ExtensionScannerInput.createNLSConfig(input)))); const cacheContents = await this._readExtensionCache(cacheKey); if (!cacheContents) { @@ -155,10 +177,10 @@ export class CachedExtensionScanner { } } - private async _scanExtensionsWithCache(cacheKey: string, input: ExtensionScannerInput, log: ILog): Promise { + private async _scanExtensionsWithCache(cacheKey: string, input: ExtensionScannerInput): Promise { if (input.devMode) { // Do not cache when running out of sources... - return ExtensionScanner.scanExtensions(input, log, this._fileService); + return this.scanExtensionDescriptions(input.isBuiltin, ExtensionScannerInput.createNLSConfig(input)); } try { @@ -187,10 +209,18 @@ export class CachedExtensionScanner { }); } - const counterLogger = new CounterLogger(log); - const result = await ExtensionScanner.scanExtensions(input, counterLogger, this._fileService); - if (counterLogger.errorCnt === 0) { - // Nothing bad happened => cache the result + const result: IExtensionDescription[] = []; + let canCache = true; + const scannedExtensions = await this.scanExtensions(input.isBuiltin, ExtensionScannerInput.createNLSConfig(input), true); + for (const scannedExtension of scannedExtensions) { + if (scannedExtension.isValid) { + result.push(toExtensionDescription(scannedExtension, input.isUnderDevelopment)); + } else { + // Do not cache if any of the extensions are not valid + canCache = false; + } + } + if (canCache) { const cacheContents: IExtensionCacheData = { input: input, result: result @@ -214,7 +244,6 @@ export class CachedExtensionScanner { } private async _scanInstalledExtensions( - log: ILog, translations: Translations ): Promise<{ system: IExtensionDescription[]; user: IExtensionDescription[]; development: IExtensionDescription[] }> { @@ -223,135 +252,42 @@ export class CachedExtensionScanner { const date = this._productService.date; const devMode = !this._environmentService.isBuilt; const locale = platform.language; - const targetPlatform = await this._extensionManagementService.getTargetPlatform(); const builtinExtensions = this._scanExtensionsWithCache( BUILTIN_MANIFEST_CACHE_FILE, - new ExtensionScannerInput(version, date, commit, locale, devMode, getSystemExtensionsRoot(), true, false, targetPlatform, translations), - log + new ExtensionScannerInput(version, date, commit, locale, devMode, this._extensionsScannerService.systemExtensionsLocation.path, true, false, translations), ); - let finalBuiltinExtensions: Promise = builtinExtensions; - - if (devMode) { - const builtInExtensions = Promise.resolve(this._productService.builtInExtensions || []); - - const controlFilePath = joinPath(this._environmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json').fsPath; - const controlFile = this._fileService.readFile(URI.file(controlFilePath)) - .then(raw => JSON.parse(raw.value.toString()), () => ({} as any)); - - const input = new ExtensionScannerInput(version, date, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, targetPlatform, translations); - const extraBuiltinExtensions = Promise.all([builtInExtensions, controlFile]) - .then(([builtInExtensions, control]) => new ExtraBuiltInExtensionResolver(builtInExtensions, control)) - .then(resolver => ExtensionScanner.scanExtensions(input, log, this._fileService, resolver)); - - finalBuiltinExtensions = ExtensionScanner.mergeBuiltinExtensions(builtinExtensions, extraBuiltinExtensions); - } - - const userExtensions = (this._scanExtensionsWithCache( + const userExtensions = this._scanExtensionsWithCache( USER_MANIFEST_CACHE_FILE, - new ExtensionScannerInput(version, date, commit, locale, devMode, this._environmentService.extensionsPath, false, false, targetPlatform, translations), - log - )); + new ExtensionScannerInput(version, date, commit, locale, devMode, this._extensionsScannerService.userExtensionsLocation.path, false, false, translations), + ); // Always load developed extensions while extensions development - let developedExtensions: Promise = Promise.resolve([]); - if (this._environmentService.isExtensionDevelopment && this._environmentService.extensionDevelopmentLocationURI) { - const extDescsP = this._environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file).map(extLoc => { - return ExtensionScanner.scanOneOrMultipleExtensions( - new ExtensionScannerInput(version, date, commit, locale, devMode, originalFSPath(extLoc), false, true, targetPlatform, translations), - log, - this._fileService - ); - }); - developedExtensions = Promise.all(extDescsP).then((extDescArrays: IExtensionDescription[][]) => { - let extDesc: IExtensionDescription[] = []; - for (let eds of extDescArrays) { - extDesc = extDesc.concat(eds); - } - return extDesc; - }); - } + const nlsConfiguration = ExtensionScannerInput.createNLSConfig(ExtensionScannerInput.createNLSConfig({ devMode, locale, translations })); + const developedExtensions = this._extensionsScannerService.scanExtensionsUnderDevelopment({ nlsConfiguration }) + .then(scannedExtensions => scannedExtensions.map(e => toExtensionDescription(e, true))); - return Promise.all([finalBuiltinExtensions, userExtensions, developedExtensions]).then((extensionDescriptions: IExtensionDescription[][]) => { + return Promise.all([builtinExtensions, userExtensions, developedExtensions]).then((extensionDescriptions: IExtensionDescription[][]) => { const system = extensionDescriptions[0]; const user = extensionDescriptions[1]; const development = extensionDescriptions[2]; return { system, user, development }; }).then(undefined, err => { - log.error(`Error scanning installed extensions:`); - log.error(err); + this._logService.error(`Error scanning installed extensions:`); + this._logService.error(err); return { system: [], user: [], development: [] }; }); } -} - -interface IBuiltInExtension { - name: string; - version: string; - repo: string; -} - -interface IBuiltInExtensionControl { - [name: string]: 'marketplace' | 'disabled' | string; -} - -class ExtraBuiltInExtensionResolver implements IExtensionResolver { - - constructor(private builtInExtensions: IBuiltInExtension[], private control: IBuiltInExtensionControl) { } - - resolveExtensions(): Promise { - const result: IExtensionReference[] = []; - - for (const ext of this.builtInExtensions) { - const controlState = this.control[ext.name] || 'marketplace'; - - switch (controlState) { - case 'disabled': - break; - case 'marketplace': - result.push({ name: ext.name, path: path.join(getExtraDevSystemExtensionsRoot(), ext.name) }); - break; - default: - result.push({ name: ext.name, path: controlState }); - break; - } - } - - return Promise.resolve(result); - } -} - -class CounterLogger implements ILog { - - public errorCnt = 0; - public warnCnt = 0; - public infoCnt = 0; - - constructor(private readonly _actual: ILog) { - } - public error(message: string | Error): void { - this.errorCnt++; - this._actual.error(message); + private async scanExtensionDescriptions(isBuiltin: boolean, nlsConfiguration: NlsConfiguration): Promise { + const scannedExtensions = await this.scanExtensions(isBuiltin, nlsConfiguration, false); + return scannedExtensions.map(e => toExtensionDescription(e, false)); } - public warn(message: string): void { - this.warnCnt++; - this._actual.warn(message); + private async scanExtensions(isBuiltin: boolean, nlsConfiguration: NlsConfiguration, includeInvalid: boolean): Promise { + return isBuiltin ? this._extensionsScannerService.scanSystemExtensions({ nlsConfiguration, checkControlFile: true, includeInvalid }) + : this._extensionsScannerService.scanUserExtensions({ nlsConfiguration, includeInvalid }); } - public info(message: string): void { - this.infoCnt++; - this._actual.info(message); - } -} - -class NullLogger implements ILog { - public error(message: string | Error): void { - } - public warn(message: string): void { - } - public info(message: string): void { - } } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index dfc2bcadef0..deec2104b46 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -76,6 +76,7 @@ import 'vs/workbench/services/commands/common/commandService'; import 'vs/workbench/services/themes/browser/workbenchThemeService'; import 'vs/workbench/services/label/common/labelService'; import 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; +import 'vs/platform/extensionManagement/common/extensionsScannerService'; import 'vs/workbench/services/extensionManagement/browser/webExtensionsScannerService'; import 'vs/workbench/services/extensionManagement/browser/extensionEnablementService'; import 'vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService'; -- cgit v1.2.3 From 68c206fb1fa75e9d0065f5ba0dc320ed0d6c3f19 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 1 Apr 2022 15:58:11 +0530 Subject: add tests --- .../common/extensionsScannerService.ts | 20 +- .../test/node/extensionsScannerService.test.ts | 301 +++++++++++++++++++++ 2 files changed, 304 insertions(+), 17 deletions(-) create mode 100644 src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts (limited to 'src/vs') diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index f29b1271411..e6cc2f4aa42 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -29,7 +29,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; -type IScannedExtensionManifest = IRelaxedExtensionManifest & { __metadata?: Metadata }; +export type IScannedExtensionManifest = IRelaxedExtensionManifest & { __metadata?: Metadata }; interface IRelaxedScannedExtension { type: ExtensionType; @@ -165,12 +165,8 @@ export class NativeExtensionsScannerService extends Disposable implements INativ const promises: Promise[] = []; promises.push(this.scanDefaultSystemExtensions()); promises.push(this.scanDevSystemExtensions(scanOptions)); - try { - const [defaultSystemExtensions, devSystemExtensions] = await Promise.all(promises); - return this.applyScanOptions([...defaultSystemExtensions, ...devSystemExtensions], scanOptions, false); - } catch (error) { - throw this.joinErrors(error); - } + const [defaultSystemExtensions, devSystemExtensions] = await Promise.all(promises); + return this.applyScanOptions([...defaultSystemExtensions, ...devSystemExtensions], scanOptions, false); } async scanUserExtensions(scanOptions: ScanOptions): Promise { @@ -355,16 +351,6 @@ export class NativeExtensionsScannerService extends Disposable implements INativ return [...result.values()]; } - private joinErrors(errorOrErrors: (Error | string) | (Array)): Error { - const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors]; - if (errors.length === 1) { - return errors[0] instanceof Error ? errors[0] : new Error(errors[0]); - } - return errors.reduce((previousValue: Error, currentValue: Error | string) => { - return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`); - }, new Error('')); - } - private _devSystemExtensionsLocation: URI | null = null; private get devSystemExtensionsLocation(): URI { if (!this._devSystemExtensionsLocation) { diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts new file mode 100644 index 00000000000..60b80de354d --- /dev/null +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -0,0 +1,301 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import { tmpdir } from 'os'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { dirname, joinPath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { INativeExtensionsScannerService, IScannedExtensionManifest, NativeExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; + +const ROOT = URI.file(tmpdir()); + +suite('NativeExtensionsScanerService Test', () => { + + const disposables = new DisposableStore(); + let instantiationService: TestInstantiationService; + + setup(async () => { + instantiationService = new TestInstantiationService(); + const logService = new NullLogService(); + const fileService = disposables.add(new FileService(logService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + fileService.registerProvider(ROOT.scheme, fileSystemProvider); + instantiationService.stub(ILogService, logService); + instantiationService.stub(IFileService, fileService); + instantiationService.stub(INativeEnvironmentService, { + userHome: ROOT, + builtinExtensionsPath: joinPath(ROOT, 'system').fsPath, + extensionsPath: joinPath(ROOT, 'extensions').fsPath, + }); + instantiationService.stub(IProductService, { version: '1.66.0' }); + }); + + teardown(() => disposables.clear()); + + test('scan system extension', async () => { + const manifest: Partial = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }); + const extensionLocation = await aSystemExtension(manifest); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanSystemExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].location.toString(), extensionLocation.toString()); + assert.deepStrictEqual(actual[0].isBuiltin, true); + assert.deepStrictEqual(actual[0].type, ExtensionType.System); + assert.deepStrictEqual(actual[0].isValid, true); + assert.deepStrictEqual(actual[0].validations, []); + assert.deepStrictEqual(actual[0].metadata, undefined); + assert.deepStrictEqual(actual[0].targetPlatform, TargetPlatform.UNDEFINED); + assert.deepStrictEqual(actual[0].manifest, manifest); + }); + + test('scan user extension', async () => { + const manifest: Partial = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }); + const extensionLocation = await aUserExtension(manifest); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].location.toString(), extensionLocation.toString()); + assert.deepStrictEqual(actual[0].isBuiltin, false); + assert.deepStrictEqual(actual[0].type, ExtensionType.User); + assert.deepStrictEqual(actual[0].isValid, true); + assert.deepStrictEqual(actual[0].validations, []); + assert.deepStrictEqual(actual[0].metadata, undefined); + assert.deepStrictEqual(actual[0].targetPlatform, TargetPlatform.UNDEFINED); + assert.deepStrictEqual(actual[0].manifest, manifest); + }); + + test('scan existing extension', async () => { + const manifest: Partial = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }); + const extensionLocation = await aUserExtension(manifest); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanExistingExtension(extensionLocation, ExtensionType.User, {}); + + assert.notEqual(actual, null); + assert.deepStrictEqual(actual!.identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual!.location.toString(), extensionLocation.toString()); + assert.deepStrictEqual(actual!.isBuiltin, false); + assert.deepStrictEqual(actual!.type, ExtensionType.User); + assert.deepStrictEqual(actual!.isValid, true); + assert.deepStrictEqual(actual!.validations, []); + assert.deepStrictEqual(actual!.metadata, undefined); + assert.deepStrictEqual(actual!.targetPlatform, TargetPlatform.UNDEFINED); + assert.deepStrictEqual(actual!.manifest, manifest); + }); + + test('scan single extension', async () => { + const manifest: Partial = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }); + const extensionLocation = await aUserExtension(manifest); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanOneOrMultipleExtensions(extensionLocation, ExtensionType.User, {}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].location.toString(), extensionLocation.toString()); + assert.deepStrictEqual(actual[0].isBuiltin, false); + assert.deepStrictEqual(actual[0].type, ExtensionType.User); + assert.deepStrictEqual(actual[0].isValid, true); + assert.deepStrictEqual(actual[0].validations, []); + assert.deepStrictEqual(actual[0].metadata, undefined); + assert.deepStrictEqual(actual[0].targetPlatform, TargetPlatform.UNDEFINED); + assert.deepStrictEqual(actual[0].manifest, manifest); + }); + + test('scan multiple extensions', async () => { + const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanOneOrMultipleExtensions(dirname(extensionLocation), ExtensionType.User, {}); + + assert.deepStrictEqual(actual.length, 2); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' }); + }); + + test('scan user extension with different versions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' })); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].manifest.version, '1.0.2'); + }); + + test('scan user extension include all versions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' })); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({ includeAllVersions: true }); + + assert.deepStrictEqual(actual.length, 2); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].manifest.version, '1.0.1'); + assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[1].manifest.version, '1.0.2'); + }); + + test('scan user extension with different versions and higher version is not compatible', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2', engines: { vscode: '^1.67.0' } })); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].manifest.version, '1.0.1'); + }); + + test('scan exclude invalid extensions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + }); + + test('scan exclude uninstalled extensions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); + await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + }); + + test('scan include uninstalled extensions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); + await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({ includeUninstalled: true }); + + assert.deepStrictEqual(actual.length, 2); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' }); + }); + + test('scan include invalid extensions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({ includeInvalid: true }); + + assert.deepStrictEqual(actual.length, 2); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' }); + }); + + test('scan system extensions include additional builtin extensions', async () => { + instantiationService.stub(IProductService, { + version: '1.66.0', + builtInExtensions: [ + { name: 'pub.name2', version: '', repo: '', metadata: undefined }, + { name: 'pub.name', version: '', repo: '', metadata: undefined } + ] + }); + await anExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' }), joinPath(ROOT, 'additional')); + const extensionLocation = await anExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }), joinPath(ROOT, 'additional')); + await aSystemExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); + await instantiationService.get(IFileService).writeFile(joinPath(instantiationService.get(INativeEnvironmentService).userHome, '.vscode-oss-dev', 'extensions', 'control.json'), VSBuffer.fromString(JSON.stringify({ 'pub.name2': 'disabled', 'pub.name': extensionLocation.fsPath }))); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanSystemExtensions({ checkControlFile: true }); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].manifest.version, '1.0.0'); + }); + + test('scan extension with default nls replacements', async () => { + const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' })); + await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' }))); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World'); + }); + + test('scan extension with en nls replacements', async () => { + const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' })); + await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' }))); + const nlsLocation = joinPath(extensionLocation, 'package.en.json'); + await instantiationService.get(IFileService).writeFile(nlsLocation, VSBuffer.fromString(JSON.stringify({ contents: { package: { displayName: 'Hello World EN' } } }))); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({ nlsConfiguration: { locale: 'en', devMode: false, pseudo: false, translations: { 'pub.name': nlsLocation.fsPath } } }); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World EN'); + }); + + test('scan extension falls back to default nls replacements', async () => { + const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' })); + await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' }))); + const nlsLocation = joinPath(extensionLocation, 'package.en.json'); + await instantiationService.get(IFileService).writeFile(nlsLocation, VSBuffer.fromString(JSON.stringify({ contents: { package: { displayName: 'Hello World EN' } } }))); + const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({ nlsConfiguration: { locale: 'en', devMode: false, pseudo: false, translations: { 'pub.name2': nlsLocation.fsPath } } }); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World'); + }); + + async function aUserExtension(manifest: Partial): Promise { + const environmentService = instantiationService.get(INativeEnvironmentService); + return anExtension(manifest, URI.parse(environmentService.extensionsPath)); + } + + async function aSystemExtension(manifest: Partial): Promise { + const environmentService = instantiationService.get(INativeEnvironmentService); + return anExtension(manifest, URI.parse(environmentService.builtinExtensionsPath)); + } + + async function anExtension(manifest: Partial, root: URI): Promise { + const fileService = instantiationService.get(IFileService); + const extensionLocation = joinPath(root, `${manifest.publisher}.${manifest.name}-${manifest.version}-${manifest.__metadata?.targetPlatform ?? TargetPlatform.UNDEFINED}`); + await fileService.writeFile(joinPath(extensionLocation, 'package.json'), VSBuffer.fromString(JSON.stringify(manifest))); + return extensionLocation; + } + + function anExtensionManifest(manifest: Partial): Partial { + return { engines: { vscode: '^1.66.0' }, version: '1.0.0', main: 'main.js', activationEvents: ['*'], ...manifest }; + } +}); -- cgit v1.2.3 From 424cc864b537c39c57de68c8a2564595a16ab8ea Mon Sep 17 00:00:00 2001 From: Leonardo Montini Date: Fri, 1 Apr 2022 15:06:08 +0200 Subject: 143543 replaced true and false with on and off in debug.inlineValues --- src/vs/workbench/contrib/debug/browser/debug.contribution.ts | 2 +- src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts | 2 +- src/vs/workbench/contrib/debug/common/debug.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/vs') diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 11f4ef0ce63..8330e2aab7c 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -419,7 +419,7 @@ configurationRegistry.registerConfiguration({ }, 'debug.inlineValues': { type: ['boolean', 'string'], - 'enum': [true, false, 'auto'], + 'enum': ['on', 'off', 'auto'], description: nls.localize({ comment: ['This is the description for a setting'], key: 'inlineValues' }, "Show variable values inline in editor while debugging."), 'enumDescriptions': [ nls.localize('inlineValues.on', 'Always show variable values inline in editor while debugging.'), diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 81133f8fade..a0fbf2da5a1 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -627,7 +627,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { const model = this.editor.getModel(); const inlineValuesSetting = this.configurationService.getValue('debug').inlineValues; - const inlineValuesTurnedOn = inlineValuesSetting === true || (inlineValuesSetting === 'auto' && model && this.languageFeaturesService.inlineValuesProvider.has(model)); + const inlineValuesTurnedOn = inlineValuesSetting === true || inlineValuesSetting === 'on' || (inlineValuesSetting === 'auto' && model && this.languageFeaturesService.inlineValuesProvider.has(model)); if (!inlineValuesTurnedOn || !model || !stackFrame || model.uri.toString() !== stackFrame.source.uri.toString()) { if (!this.removeInlineValuesScheduler.isScheduled()) { this.removeInlineValuesScheduler.schedule(); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 7fd322d2d11..be21bf957ef 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -611,7 +611,7 @@ export interface IDebugConfiguration { allowBreakpointsEverywhere: boolean; openDebug: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart' | 'openOnDebugBreak'; openExplorerOnEnd: boolean; - inlineValues: boolean | 'auto'; + inlineValues: boolean | 'auto' | 'on' | 'off'; toolBarLocation: 'floating' | 'docked' | 'hidden'; showInStatusBar: 'never' | 'always' | 'onFirstSessionStart'; internalConsoleOptions: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart'; -- cgit v1.2.3 From 6e1e0b1d899b2d262265e8faeadee98c562cfe7b Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 1 Apr 2022 17:35:06 +0200 Subject: Moves tokenization logic from text model to its own text model part. --- .../editor/browser/services/editorWorkerService.ts | 2 +- src/vs/editor/browser/widget/diffEditorWidget.ts | 2 +- src/vs/editor/common/commands/shiftCommand.ts | 2 +- .../editor/common/cursor/cursorTypeOperations.ts | 20 +- src/vs/editor/common/languages/autoIndent.ts | 36 +- .../languages/languageConfigurationRegistry.ts | 4 +- src/vs/editor/common/model.ts | 106 +--- .../bracketPairsTextModelPart/bracketPairsImpl.ts | 12 +- .../bracketPairsTree/bracketPairsTree.ts | 11 +- .../bracketPairsTree/tokenizer.ts | 9 +- .../model/bracketPairsTextModelPart/fixBrackets.ts | 9 +- src/vs/editor/common/model/textModel.ts | 406 ++------------ src/vs/editor/common/model/textModelPart.ts | 7 +- src/vs/editor/common/model/textModelTokens.ts | 20 +- .../common/model/tokenizationTextModelPart.ts | 610 +++++++++++++++++++++ .../common/services/markerDecorationsService.ts | 2 +- src/vs/editor/common/services/modelService.ts | 12 +- src/vs/editor/common/standalone/standaloneEnums.ts | 6 + .../editor/common/viewModel/modelLineProjection.ts | 10 +- .../common/viewModel/viewModelDecorations.ts | 2 +- src/vs/editor/common/viewModel/viewModelImpl.ts | 4 +- src/vs/editor/common/viewModel/viewModelLines.ts | 2 +- .../contrib/comment/browser/blockCommentCommand.ts | 2 +- .../contrib/comment/browser/lineCommentCommand.ts | 4 +- .../gotoError/browser/markerNavigationService.ts | 2 +- .../contrib/gotoSymbol/browser/goToCommands.ts | 2 +- .../browser/link/goToDefinitionAtPosition.ts | 2 +- .../contrib/gotoSymbol/browser/referencesModel.ts | 2 +- .../contrib/indentation/browser/indentation.ts | 24 +- .../contrib/inlayHints/browser/inlayHints.ts | 6 +- .../browser/inlineCompletionsModel.ts | 4 +- .../test/browser/suggestWidgetModel.test.ts | 2 +- .../linesOperations/browser/moveLinesCommand.ts | 24 +- .../test/browser/linkedEditing.test.ts | 2 +- src/vs/editor/contrib/rename/browser/rename.ts | 2 +- .../contrib/smartSelect/browser/wordSelections.ts | 4 +- .../contrib/snippet/browser/snippetController2.ts | 2 +- .../contrib/snippet/browser/snippetVariables.ts | 2 +- src/vs/editor/contrib/suggest/browser/suggest.ts | 2 +- .../suggest/browser/suggestInlineCompletions.ts | 6 +- .../contrib/suggest/browser/suggestMemory.ts | 4 +- .../editor/contrib/suggest/browser/suggestModel.ts | 10 +- .../contrib/suggest/browser/wordContextKey.ts | 2 +- .../editor/contrib/suggest/browser/wordDistance.ts | 2 +- .../suggest/test/browser/suggestModel.test.ts | 6 +- .../contrib/tokenization/browser/tokenization.ts | 4 +- .../browser/viewportSemanticTokens.ts | 12 +- .../wordHighlighter/browser/wordHighlighter.ts | 6 +- src/vs/editor/standalone/browser/colorizer.ts | 4 +- .../editor/standalone/browser/standaloneEditor.ts | 1 + .../standalone/browser/standaloneLanguages.ts | 2 +- .../editor/test/browser/controller/cursor.test.ts | 62 +-- src/vs/editor/test/browser/testCommand.ts | 2 +- .../browser/viewModel/modelLineProjection.test.ts | 8 +- .../model/bracketPairColorizer/tokenizer.test.ts | 2 +- src/vs/editor/test/common/model/model.line.test.ts | 4 +- .../editor/test/common/model/model.modes.test.ts | 72 +-- src/vs/editor/test/common/model/model.test.ts | 52 +- .../test/common/model/textModelWithTokens.test.ts | 18 +- .../editor/test/common/model/tokensStore.test.ts | 8 +- src/vs/monaco.d.ts | 39 +- .../workbench/api/browser/mainThreadLanguages.ts | 4 +- .../bulkEdit/browser/preview/bulkEditTree.ts | 4 +- .../contrib/debug/browser/debugEditorActions.ts | 2 +- .../debug/browser/debugEditorContribution.ts | 4 +- src/vs/workbench/contrib/debug/browser/repl.ts | 2 +- .../contrib/emmet/browser/emmetActions.ts | 2 +- .../browser/extensionsCompletionItemsProvider.ts | 2 +- .../browser/view/cellParts/cellDragRenderer.ts | 2 +- .../workbench/contrib/search/browser/searchView.ts | 2 +- .../searchEditor/browser/searchEditorActions.ts | 2 +- .../contrib/snippets/browser/insertSnippet.ts | 2 +- .../snippets/browser/snippetCompletionProvider.ts | 4 +- .../snippets/browser/surroundWithSnippet.ts | 2 +- .../contrib/snippets/browser/tabCompletion.ts | 2 +- .../preferences/common/preferencesModels.ts | 2 +- .../textMate/browser/nativeTextMateService.ts | 2 +- 77 files changed, 1005 insertions(+), 743 deletions(-) create mode 100644 src/vs/editor/common/model/tokenizationTextModelPart.ts (limited to 'src/vs') diff --git a/src/vs/editor/browser/services/editorWorkerService.ts b/src/vs/editor/browser/services/editorWorkerService.ts index 729367dd0c0..bb40eed6a39 100644 --- a/src/vs/editor/browser/services/editorWorkerService.ts +++ b/src/vs/editor/browser/services/editorWorkerService.ts @@ -193,7 +193,7 @@ class WordBasedCompletionItemProvider implements languages.CompletionItemProvide } const wordDefRegExp = this.languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition(); - const word = model.getWordAtPosition(position); + const word = model.tokenization.getWordAtPosition(position); const replace = !word ? Range.fromPositions(position) : new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn); const insert = replace.setEndPosition(position.lineNumber, position.column); diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 22af65a26f5..a29e3b980fc 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -2333,7 +2333,7 @@ class InlineViewZonesComputer extends ViewZonesComputer { let viewLineCounts: number[] | null = null; for (let lineNumber = lineChange.originalStartLineNumber; lineNumber <= lineChange.originalEndLineNumber; lineNumber++) { const lineIndex = lineNumber - lineChange.originalStartLineNumber; - const lineTokens = this._originalModel.getLineTokens(lineNumber); + const lineTokens = this._originalModel.tokenization.getLineTokens(lineNumber); const lineContent = lineTokens.getLineContent(); const lineBreakData = lineBreaks[lineBreakIndex++]; const actualDecorations = LineDecoration.filter(decorations, lineNumber, 1, lineContent.length + 1); diff --git a/src/vs/editor/common/commands/shiftCommand.ts b/src/vs/editor/common/commands/shiftCommand.ts index 1352d7a60f9..81f552ecd1f 100644 --- a/src/vs/editor/common/commands/shiftCommand.ts +++ b/src/vs/editor/common/commands/shiftCommand.ts @@ -146,7 +146,7 @@ export class ShiftCommand implements ICommand { if (contentStartVisibleColumn % indentSize !== 0) { // The current line is "miss-aligned", so let's see if this is expected... // This can only happen when it has trailing commas in the indent - if (model.isCheapToTokenize(lineNumber - 1)) { + if (model.tokenization.isCheapToTokenize(lineNumber - 1)) { const enterAction = getEnterAction(this._opts.autoIndent, model, new Range(lineNumber - 1, model.getLineMaxColumn(lineNumber - 1), lineNumber - 1, model.getLineMaxColumn(lineNumber - 1)), this._languageConfigurationService); if (enterAction) { extraSpaces = previousLineExtraSpaces; diff --git a/src/vs/editor/common/cursor/cursorTypeOperations.ts b/src/vs/editor/common/cursor/cursorTypeOperations.ts index d53f8d89ccf..1048cb59a9d 100644 --- a/src/vs/editor/common/cursor/cursorTypeOperations.ts +++ b/src/vs/editor/common/cursor/cursorTypeOperations.ts @@ -227,7 +227,7 @@ export class TypeOperations { const lineText = model.getLineContent(selection.startLineNumber); - if (/^\s*$/.test(lineText) && model.isCheapToTokenize(selection.startLineNumber)) { + if (/^\s*$/.test(lineText) && model.tokenization.isCheapToTokenize(selection.startLineNumber)) { let goodIndent = this._goodIndentForLine(config, model, selection.startLineNumber); goodIndent = goodIndent || '\t'; const possibleTypeText = config.normalizeIndentation(goodIndent); @@ -300,7 +300,7 @@ export class TypeOperations { if (config.autoIndent === EditorAutoIndentStrategy.None) { return TypeOperations._typeCommand(range, '\n', keepPosition); } - if (!model.isCheapToTokenize(range.getStartPosition().lineNumber) || config.autoIndent === EditorAutoIndentStrategy.Keep) { + if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber) || config.autoIndent === EditorAutoIndentStrategy.Keep) { const lineText = model.getLineContent(range.startLineNumber); const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); @@ -385,7 +385,7 @@ export class TypeOperations { } for (let i = 0, len = selections.length; i < len; i++) { - if (!model.isCheapToTokenize(selections[i].getEndPosition().lineNumber)) { + if (!model.tokenization.isCheapToTokenize(selections[i].getEndPosition().lineNumber)) { return false; } } @@ -642,13 +642,13 @@ export class TypeOperations { } } - if (!model.isCheapToTokenize(lineNumber)) { + if (!model.tokenization.isCheapToTokenize(lineNumber)) { // Do not force tokenization return null; } - model.forceTokenization(lineNumber); - const lineTokens = model.getLineTokens(lineNumber); + model.tokenization.forceTokenization(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); const scopedLineTokens = createScopedLineTokens(lineTokens, beforeColumn - 1); if (!pair.shouldAutoClose(scopedLineTokens, beforeColumn - scopedLineTokens.firstCharOffset)) { return null; @@ -664,7 +664,7 @@ export class TypeOperations { // const neutralCharacter = pair.findNeutralCharacter(); if (neutralCharacter) { - const tokenType = model.getTokenTypeIfInsertingCharacter(lineNumber, beforeColumn, neutralCharacter); + const tokenType = model.tokenization.getTokenTypeIfInsertingCharacter(lineNumber, beforeColumn, neutralCharacter); if (!pair.isOK(tokenType)) { return null; } @@ -757,7 +757,7 @@ export class TypeOperations { } private static _isTypeInterceptorElectricChar(config: CursorConfiguration, model: ITextModel, selections: Selection[]) { - if (selections.length === 1 && model.isCheapToTokenize(selections[0].getEndPosition().lineNumber)) { + if (selections.length === 1 && model.tokenization.isCheapToTokenize(selections[0].getEndPosition().lineNumber)) { return true; } return false; @@ -769,8 +769,8 @@ export class TypeOperations { } const position = selection.getPosition(); - model.forceTokenization(position.lineNumber); - const lineTokens = model.getLineTokens(position.lineNumber); + model.tokenization.forceTokenization(position.lineNumber); + const lineTokens = model.tokenization.getLineTokens(position.lineNumber); let electricAction: IElectricAction | null; try { diff --git a/src/vs/editor/common/languages/autoIndent.ts b/src/vs/editor/common/languages/autoIndent.ts index d588ea766a8..44c9b30467d 100644 --- a/src/vs/editor/common/languages/autoIndent.ts +++ b/src/vs/editor/common/languages/autoIndent.ts @@ -14,9 +14,11 @@ import { getScopedLineTokens, ILanguageConfigurationService } from 'vs/editor/co import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; export interface IVirtualModel { - getLineTokens(lineNumber: number): LineTokens; - getLanguageId(): string; - getLanguageIdAtPosition(lineNumber: number, column: number): string; + tokenization: { + getLineTokens(lineNumber: number): LineTokens; + getLanguageId(): string; + getLanguageIdAtPosition(lineNumber: number, column: number): string; + }; getLineContent(lineNumber: number): string; } @@ -34,13 +36,13 @@ export interface IIndentConverter { * else: nearest preceding line of the same language */ function getPrecedingValidLine(model: IVirtualModel, lineNumber: number, indentRulesSupport: IndentRulesSupport) { - const languageId = model.getLanguageIdAtPosition(lineNumber, 0); + const languageId = model.tokenization.getLanguageIdAtPosition(lineNumber, 0); if (lineNumber > 1) { let lastLineNumber: number; let resultLineNumber = -1; for (lastLineNumber = lineNumber - 1; lastLineNumber >= 1; lastLineNumber--) { - if (model.getLanguageIdAtPosition(lastLineNumber, 0) !== languageId) { + if (model.tokenization.getLanguageIdAtPosition(lastLineNumber, 0) !== languageId) { return resultLineNumber; } const text = model.getLineContent(lastLineNumber); @@ -79,7 +81,7 @@ export function getInheritIndentForLine( return null; } - const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentRulesSupport; + const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.tokenization.getLanguageId()).indentRulesSupport; if (!indentRulesSupport) { return null; } @@ -283,8 +285,8 @@ export function getIndentForEnter( if (autoIndent < EditorAutoIndentStrategy.Full) { return null; } - model.forceTokenization(range.startLineNumber); - const lineTokens = model.getLineTokens(range.startLineNumber); + model.tokenization.forceTokenization(range.startLineNumber); + const lineTokens = model.tokenization.getLineTokens(range.startLineNumber); const scopedLineTokens = createScopedLineTokens(lineTokens, range.startColumn - 1); const scopedLineText = scopedLineTokens.getLineContent(); @@ -315,14 +317,16 @@ export function getIndentForEnter( const beforeEnterIndent = strings.getLeadingWhitespace(beforeEnterText); const virtualModel: IVirtualModel = { - getLineTokens: (lineNumber: number) => { - return model.getLineTokens(lineNumber); - }, - getLanguageId: () => { - return model.getLanguageId(); - }, - getLanguageIdAtPosition: (lineNumber: number, column: number) => { - return model.getLanguageIdAtPosition(lineNumber, column); + tokenization: { + getLineTokens: (lineNumber: number) => { + return model.tokenization.getLineTokens(lineNumber); + }, + getLanguageId: () => { + return model.getLanguageId(); + }, + getLanguageIdAtPosition: (lineNumber: number, column: number) => { + return model.getLanguageIdAtPosition(lineNumber, column); + }, }, getLineContent: (lineNumber: number) => { if (lineNumber === range.startLineNumber) { diff --git a/src/vs/editor/common/languages/languageConfigurationRegistry.ts b/src/vs/editor/common/languages/languageConfigurationRegistry.ts index 2b641d53c0e..3a86fa34177 100644 --- a/src/vs/editor/common/languages/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/languages/languageConfigurationRegistry.ts @@ -179,8 +179,8 @@ export function getIndentationAtPosition(model: ITextModel, lineNumber: number, } export function getScopedLineTokens(model: ITextModel, lineNumber: number, columnNumber?: number): ScopedLineTokens { - model.forceTokenization(lineNumber); - const lineTokens = model.getLineTokens(lineNumber); + model.tokenization.forceTokenization(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); const column = (typeof columnNumber === 'undefined' ? model.getLineMaxColumn(lineNumber) - 1 : columnNumber - 1); return createScopedLineTokens(lineTokens, column); } diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 4d3ec0b94c8..2f62dbc447c 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -6,23 +6,20 @@ import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { equals } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; -import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, InternalModelContentChangeEvent, ModelInjectedTextChangedEvent } from 'vs/editor/common/textModelEvents'; -import { WordCharacterClassifier } from 'vs/editor/common/core/wordCharacterClassifier'; -import { FormattingOptions, StandardTokenType } from 'vs/editor/common/languages'; -import { ThemeColor } from 'vs/platform/theme/common/themeService'; -import { ContiguousMultilineTokens } from 'vs/editor/common/tokens/contiguousMultilineTokens'; -import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens'; import { TextChange } from 'vs/editor/common/core/textChange'; -import { equals } from 'vs/base/common/objects'; +import { WordCharacterClassifier } from 'vs/editor/common/core/wordCharacterClassifier'; +import { FormattingOptions } from 'vs/editor/common/languages'; +import { ITokenizationTextModelPart } from 'vs/editor/common/model/tokenizationTextModelPart'; import { IBracketPairsTextModelPart } from 'vs/editor/common/textModelBracketPairs'; +import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, InternalModelContentChangeEvent, ModelInjectedTextChangedEvent } from 'vs/editor/common/textModelEvents'; import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides'; -import { IWordAtPosition } from 'vs/editor/common/core/wordHelper'; -import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { ThemeColor } from 'vs/platform/theme/common/themeService'; /** * Vertical Lane in the overview ruler of the editor. @@ -779,11 +776,6 @@ export interface ITextModel { */ isDisposed(): boolean; - /** - * @internal - */ - tokenizeViewport(startLineNumber: number, endLineNumber: number): void; - /** * This model is so large that it would not be a good idea to sync it over * to web workers or other places. @@ -844,63 +836,6 @@ export interface ITextModel { */ findPreviousMatch(searchString: string, searchStart: IPosition, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean): FindMatch | null; - /** - * @internal - */ - setTokens(tokens: ContiguousMultilineTokens[]): void; - - /** - * @internal - */ - setSemanticTokens(tokens: SparseMultilineTokens[] | null, isComplete: boolean): void; - - /** - * @internal - */ - setPartialSemanticTokens(range: Range, tokens: SparseMultilineTokens[] | null): void; - - /** - * @internal - */ - hasCompleteSemanticTokens(): boolean; - - /** - * @internal - */ - hasSomeSemanticTokens(): boolean; - - /** - * Flush all tokenization state. - * @internal - */ - resetTokenization(): void; - - /** - * Force tokenization information for `lineNumber` to be accurate. - * @internal - */ - forceTokenization(lineNumber: number): void; - - /** - * If it is cheap, force tokenization information for `lineNumber` to be accurate. - * This is based on a heuristic. - * @internal - */ - tokenizeIfCheap(lineNumber: number): void; - - /** - * Check if calling `forceTokenization` for this `lineNumber` will be cheap (time-wise). - * This is based on a heuristic. - * @internal - */ - isCheapToTokenize(lineNumber: number): boolean; - - /** - * Get the tokens for the line `lineNumber`. - * The tokens might be inaccurate. Use `forceTokenization` to ensure accurate tokens. - * @internal - */ - getLineTokens(lineNumber: number): LineTokens; /** * Get the language associated with this model. @@ -920,31 +855,6 @@ export interface ITextModel { */ getLanguageIdAtPosition(lineNumber: number, column: number): string; - /** - * Returns the standard token type for a character if the character were to be inserted at - * the given position. If the result cannot be accurate, it returns null. - * @internal - */ - getTokenTypeIfInsertingCharacter(lineNumber: number, column: number, character: string): StandardTokenType; - - /** - * @internal - */ - tokenizeLineWithEdit(position: IPosition, length: number, newText: string): LineTokens | null; - - /** - * Get the word under or besides `position`. - * @param position The position to look for a word. - * @return The word under or besides `position`. Might be null. - */ - getWordAtPosition(position: IPosition): IWordAtPosition | null; - - /** - * Get the word under or besides `position` trimmed to `position`.column - * @param position The position to look for a word. - * @return The word under or besides `position`. Will never be null. - */ - getWordUntilPosition(position: IPosition): IWordAtPosition; /** * Change the decorations. The callback will be called with a change accessor @@ -1253,6 +1163,8 @@ export interface ITextModel { * @internal */ readonly guides: IGuidesTextModelPart; + + readonly tokenization: ITokenizationTextModelPart; } export const enum PositionAffinity { diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts index 2b89a098fb9..b61610f5668 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts @@ -178,7 +178,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai private _matchBracket(position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): [Range, Range] | null { const lineNumber = position.lineNumber; - const lineTokens = this.textModel.getLineTokens(lineNumber); + const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber); const lineText = this.textModel.getLineContent(lineNumber); const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); @@ -309,7 +309,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai }; for (let lineNumber = position.lineNumber; lineNumber >= 1; lineNumber--) { - const lineTokens = this.textModel.getLineTokens(lineNumber); + const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber); const tokenCount = lineTokens.getCount(); const lineText = this.textModel.getLineContent(lineNumber); @@ -397,7 +397,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai const lineCount = this.textModel.getLineCount(); for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { - const lineTokens = this.textModel.getLineTokens(lineNumber); + const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber); const tokenCount = lineTokens.getCount(); const lineText = this.textModel.getLineContent(lineNumber); @@ -454,7 +454,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai let languageId: string | null = null; let modeBrackets: RichEditBrackets | null = null; for (let lineNumber = position.lineNumber; lineNumber >= 1; lineNumber--) { - const lineTokens = this.textModel.getLineTokens(lineNumber); + const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber); const tokenCount = lineTokens.getCount(); const lineText = this.textModel.getLineContent(lineNumber); @@ -532,7 +532,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai let languageId: string | null = null; let modeBrackets: RichEditBrackets | null = null; for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { - const lineTokens = this.textModel.getLineTokens(lineNumber); + const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber); const tokenCount = lineTokens.getCount(); const lineText = this.textModel.getLineContent(lineNumber); @@ -653,7 +653,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai let languageId: string | null = null; let modeBrackets: RichEditBrackets | null = null; for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { - const lineTokens = this.textModel.getLineTokens(lineNumber); + const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber); const tokenCount = lineTokens.getCount(); const lineText = this.textModel.getLineContent(lineNumber); diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts index aa828744e7a..3c23d9b39e9 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts @@ -8,7 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { BracketInfo, BracketPairWithMinIndentationInfo } from 'vs/editor/common/textModelBracketPairs'; -import { BackgroundTokenizationState, TextModel } from 'vs/editor/common/model/textModel'; +import { TextModel } from 'vs/editor/common/model/textModel'; import { IModelContentChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents'; import { ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { AstNode, AstNodeKind } from './ast'; @@ -18,6 +18,7 @@ import { Length, lengthAdd, lengthGreaterThanEqual, lengthLessThanEqual, lengthO import { parseDocument } from './parser'; import { DenseKeyProvider } from './smallImmutableSet'; import { FastTokenizer, TextBufferTokenizer } from './tokenizer'; +import { BackgroundTokenizationState } from 'vs/editor/common/model/tokenizationTextModelPart'; export class BracketPairsTree extends Disposable { private readonly didChangeEmitter = new Emitter(); @@ -49,18 +50,18 @@ export class BracketPairsTree extends Disposable { ) { super(); - if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Uninitialized) { + if (textModel.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Uninitialized) { // There are no token information yet const brackets = this.brackets.getSingleLanguageBracketTokens(this.textModel.getLanguageId()); const tokenizer = new FastTokenizer(this.textModel.getValue(), brackets); this.initialAstWithoutTokens = parseDocument(tokenizer, [], undefined, true); this.astWithTokens = this.initialAstWithoutTokens; - } else if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Completed) { + } else if (textModel.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) { // Skip the initial ast, as there is no flickering. // Directly create the tree with token information. this.initialAstWithoutTokens = undefined; this.astWithTokens = this.parseDocumentFromTextBuffer([], undefined, false); - } else if (textModel.backgroundTokenizationState === BackgroundTokenizationState.InProgress) { + } else if (textModel.tokenization.backgroundTokenizationState === BackgroundTokenizationState.InProgress) { this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer([], undefined, true); this.astWithTokens = this.initialAstWithoutTokens; } @@ -69,7 +70,7 @@ export class BracketPairsTree extends Disposable { //#region TextModel events public handleDidChangeBackgroundTokenizationState(): void { - if (this.textModel.backgroundTokenizationState === BackgroundTokenizationState.Completed) { + if (this.textModel.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) { const wasUndefined = this.initialAstWithoutTokens === undefined; // Clear the initial tree as we can use the tree with token information now. this.initialAstWithoutTokens = undefined; diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts index 0c572140a2d..c13914dbf3a 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts @@ -54,7 +54,10 @@ export interface ITokenizerSource { getValue(): string; getLineCount(): number; getLineLength(lineNumber: number): number; - getLineTokens(lineNumber: number): IViewLineTokens; + + tokenization: { + getLineTokens(lineNumber: number): IViewLineTokens; + }; } export class TextBufferTokenizer implements Tokenizer { @@ -166,7 +169,7 @@ class NonPeekableTextBufferTokenizer { } if (this.line === null) { - this.lineTokens = this.textModel.getLineTokens(this.lineIdx + 1); + this.lineTokens = this.textModel.tokenization.getLineTokens(this.lineIdx + 1); this.line = this.lineTokens.getLineContent(); this.lineTokenOffset = this.lineCharOffset === 0 ? 0 : this.lineTokens!.findTokenIndexAtOffset(this.lineCharOffset); } @@ -238,7 +241,7 @@ class NonPeekableTextBufferTokenizer { break; } this.lineIdx++; - this.lineTokens = this.textModel.getLineTokens(this.lineIdx + 1); + this.lineTokens = this.textModel.tokenization.getLineTokens(this.lineIdx + 1); this.lineTokenOffset = 0; this.line = this.lineTokens.getLineContent(); this.lineCharOffset = 0; diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/fixBrackets.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/fixBrackets.ts index 866cd711a70..d9db755673f 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/fixBrackets.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/fixBrackets.ts @@ -76,7 +76,10 @@ class StaticTokenizerSource implements ITokenizerSource { getLineLength(lineNumber: number): number { return this.lines[lineNumber - 1].getLineContent().length; } - getLineTokens(lineNumber: number): IViewLineTokens { - return this.lines[lineNumber - 1]; - } + + tokenization = { + getLineTokens: (lineNumber: number): IViewLineTokens => { + return this.lines[lineNumber - 1]; + } + }; } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 78f17dd2669..153367f8fd2 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -5,7 +5,6 @@ import { ArrayQueue, pushMany } from 'vs/base/common/arrays'; import { VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; -import { CharCode } from 'vs/base/common/charCode'; import { Color } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; @@ -15,39 +14,33 @@ import { listenStream } from 'vs/base/common/stream'; import * as strings from 'vs/base/common/strings'; import { Constants } from 'vs/base/common/uint'; import { URI } from 'vs/base/common/uri'; -import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { countEOL } from 'vs/editor/common/core/eolCounter'; +import { normalizeIndentation } from 'vs/editor/common/core/indentation'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; +import { TextChange } from 'vs/editor/common/core/textChange'; +import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/core/textModelDefaults'; +import { FormattingOptions } from 'vs/editor/common/languages'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import * as model from 'vs/editor/common/model'; -import { IBracketPairsTextModelPart } from 'vs/editor/common/textModelBracketPairs'; import { BracketPairsTextModelPart } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl'; import { ColorizedBracketPairsDecorationProvider } from 'vs/editor/common/model/bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider'; import { EditStack } from 'vs/editor/common/model/editStack'; import { GuidesTextModelPart } from 'vs/editor/common/model/guidesTextModelPart'; -import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides'; import { guessIndentation } from 'vs/editor/common/model/indentationGuesser'; import { IntervalNode, IntervalTree, recomputeMaxEnd } from 'vs/editor/common/model/intervalTree'; import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; -import { TextChange } from 'vs/editor/common/core/textChange'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/textModelEvents'; import { SearchParams, TextModelSearch } from 'vs/editor/common/model/textModelSearch'; -import { TextModelTokenization } from 'vs/editor/common/model/textModelTokens'; -import { countEOL } from 'vs/editor/common/core/eolCounter'; -import { ContiguousMultilineTokens } from 'vs/editor/common/tokens/contiguousMultilineTokens'; -import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens'; -import { ContiguousTokensStore } from 'vs/editor/common/tokens/contiguousTokensStore'; -import { SparseTokensStore } from 'vs/editor/common/tokens/sparseTokensStore'; -import { getWordAtText, IWordAtPosition } from 'vs/editor/common/core/wordHelper'; -import { FormattingOptions, StandardTokenType } from 'vs/editor/common/languages'; -import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ITokenizationTextModelPart, TokenizationTextModelPart } from 'vs/editor/common/model/tokenizationTextModelPart'; +import { IBracketPairsTextModelPart } from 'vs/editor/common/textModelBracketPairs'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptionsChangedEvent, InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/textModelEvents'; +import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides'; import { IColorTheme, ThemeColor } from 'vs/platform/theme/common/themeService'; import { IUndoRedoService, ResourceEditStackSnapshot } from 'vs/platform/undoRedo/common/undoRedo'; -import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/core/textModelDefaults'; -import { normalizeIndentation } from 'vs/editor/common/core/indentation'; -import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; function createTextBufferBuilder() { return new PieceTreeTextBufferBuilder(); @@ -172,12 +165,6 @@ const enum StringOffsetValidationType { SurrogatePairs = 1, } -export const enum BackgroundTokenizationState { - Uninitialized = 0, - InProgress = 1, - Completed = 2, -} - export class TextModel extends Disposable implements model.ITextModel, IDecorationsTreesHost { private static readonly MODEL_SYNC_LIMIT = 50 * 1024 * 1024; // 50 MB @@ -227,14 +214,9 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private readonly _onDidChangeDecorations: DidChangeDecorationsEmitter = this._register(new DidChangeDecorationsEmitter(affectedInjectedTextLines => this.handleBeforeFireDecorationsChangedEvent(affectedInjectedTextLines))); public readonly onDidChangeDecorations: Event = this._onDidChangeDecorations.event; - private readonly _onDidChangeLanguage: Emitter = this._register(new Emitter()); - public readonly onDidChangeLanguage: Event = this._onDidChangeLanguage.event; - - private readonly _onDidChangeLanguageConfiguration: Emitter = this._register(new Emitter()); - public readonly onDidChangeLanguageConfiguration: Event = this._onDidChangeLanguageConfiguration.event; - - private readonly _onDidChangeTokens: Emitter = this._register(new Emitter()); - public readonly onDidChangeTokens: Event = this._onDidChangeTokens.event; + public get onDidChangeLanguage() { return this._tokenizationTextModelPart.onDidChangeLanguage; } + public get onDidChangeLanguageConfiguration() { return this._tokenizationTextModelPart.onDidChangeLanguageConfiguration; } + public get onDidChangeTokens() { return this._tokenizationTextModelPart.onDidChangeTokens; } private readonly _onDidChangeOptions: Emitter = this._register(new Emitter()); public readonly onDidChangeOptions: Event = this._onDidChangeOptions.event; @@ -265,7 +247,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private _options: model.TextModelResolvedOptions; private _isDisposed: boolean; - private _isDisposing: boolean; + private __isDisposing: boolean; + public _isDisposing(): boolean { return this.__isDisposing; } private _versionId: number; /** * Unlike, versionId, this can go down (via undo) or go to previous values (via redo) @@ -294,40 +277,15 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private readonly _decorationProvider: ColorizedBracketPairsDecorationProvider; //#endregion - //#region Tokenization - private _languageId: string; - private readonly _languageRegistryListener: IDisposable; - private readonly _tokens: ContiguousTokensStore; - private readonly _semanticTokens: SparseTokensStore; - private readonly _tokenization: TextModelTokenization; - //#endregion + private readonly _tokenizationTextModelPart: TokenizationTextModelPart; + public get tokenization(): ITokenizationTextModelPart { return this._tokenizationTextModelPart; } - private readonly _bracketPairColorizer: BracketPairsTextModelPart; - public get bracketPairs(): IBracketPairsTextModelPart { return this._bracketPairColorizer; } + private readonly _bracketPairs: BracketPairsTextModelPart; + public get bracketPairs(): IBracketPairsTextModelPart { return this._bracketPairs; } private readonly _guidesTextModelPart: GuidesTextModelPart; public get guides(): IGuidesTextModelPart { return this._guidesTextModelPart; } - private _backgroundTokenizationState = BackgroundTokenizationState.Uninitialized; - public get backgroundTokenizationState(): BackgroundTokenizationState { - return this._backgroundTokenizationState; - } - private handleTokenizationProgress(completed: boolean) { - if (this._backgroundTokenizationState === BackgroundTokenizationState.Completed) { - // We already did a full tokenization and don't go back to progressing. - return; - } - const newState = completed ? BackgroundTokenizationState.Completed : BackgroundTokenizationState.InProgress; - if (this._backgroundTokenizationState !== newState) { - this._backgroundTokenizationState = newState; - this._bracketPairColorizer.handleDidChangeBackgroundTokenizationState(); - this._onBackgroundTokenizationStateChanged.fire(); - } - } - - private readonly _onBackgroundTokenizationStateChanged = this._register(new Emitter()); - public readonly onBackgroundTokenizationStateChanged: Event = this._onBackgroundTokenizationStateChanged.event; - constructor( source: string | model.ITextBufferFactory, languageId: string, @@ -356,6 +314,17 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._options = TextModel.resolveOptions(this._buffer, creationOptions); + this._bracketPairs = this._register(new BracketPairsTextModelPart(this, this._languageConfigurationService)); + this._guidesTextModelPart = this._register(new GuidesTextModelPart(this, this._languageConfigurationService)); + this._decorationProvider = this._register(new ColorizedBracketPairsDecorationProvider(this)); + this._tokenizationTextModelPart = new TokenizationTextModelPart( + this._languageService, + this._languageConfigurationService, + this, + this._bracketPairs, + languageId + ); + const bufferLineCount = this._buffer.getLineCount(); const bufferTextLength = this._buffer.getValueLengthInRange(new Range(1, 1, bufferLineCount, this._buffer.getLineLength(bufferLineCount) + 1), model.EndOfLinePreference.TextDefined); @@ -378,17 +347,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._initialUndoRedoSnapshot = null; this._isDisposed = false; - this._isDisposing = false; - - this._languageId = languageId; - - this._languageRegistryListener = this._languageConfigurationService.onDidChange( - e => { - if (e.affects(this._languageId)) { - this._onDidChangeLanguageConfiguration.fire({}); - } - } - ); + this.__isDisposing = false; this._instanceId = strings.singleLetterHash(MODEL_ID); this._lastDecorationId = 0; @@ -400,13 +359,6 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._isRedoing = false; this._trimAutoWhitespaceLines = null; - this._tokens = new ContiguousTokensStore(this._languageService.languageIdCodec); - this._semanticTokens = new SparseTokensStore(this._languageService.languageIdCodec); - this._tokenization = new TextModelTokenization(this, this._languageService.languageIdCodec); - - this._bracketPairColorizer = this._register(new BracketPairsTextModelPart(this, this._languageConfigurationService)); - this._guidesTextModelPart = this._register(new GuidesTextModelPart(this, this._languageConfigurationService)); - this._decorationProvider = this._register(new ColorizedBracketPairsDecorationProvider(this)); this._register(this._decorationProvider.onDidChange(() => { this._onDidChangeDecorations.beginDeferredEmit(); @@ -416,14 +368,13 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } public override dispose(): void { - this._isDisposing = true; + this.__isDisposing = true; this._onWillDispose.fire(); - this._languageRegistryListener.dispose(); - this._tokenization.dispose(); + this._tokenizationTextModelPart.dispose(); this._isDisposed = true; super.dispose(); this._bufferDisposable.dispose(); - this._isDisposing = false; + this.__isDisposing = false; // Manually release reference to previous text buffer to avoid large leaks // in case someone leaks a TextModel reference const emptyDisposedTextBuffer = new PieceTreeTextBuffer([], '', '\n', false, false, true, true); @@ -436,14 +387,11 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return ( this._onWillDispose.hasListeners() || this._onDidChangeDecorations.hasListeners() - || this._onDidChangeLanguage.hasListeners() - || this._onDidChangeLanguageConfiguration.hasListeners() - || this._onDidChangeTokens.hasListeners() + || this._tokenizationTextModelPart._hasListeners() || this._onDidChangeOptions.hasListeners() || this._onDidChangeAttached.hasListeners() || this._onDidChangeInjectedText.hasListeners() || this._eventEmitter.hasListeners() - || this._onBackgroundTokenizationStateChanged.hasListeners() ); } @@ -464,12 +412,12 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } private _emitContentChangedEvent(rawChange: ModelRawContentChangedEvent, change: IModelContentChangedEvent): void { - if (this._isDisposing) { + if (this.__isDisposing) { // Do not confuse listeners by emitting any event after disposing return; } - this._bracketPairColorizer.handleDidChangeContent(change); - this._tokenization.handleDidChangeContent(change); + this._bracketPairs.handleDidChangeContent(change); + this._tokenizationTextModelPart.handleDidChangeContent(change); this._eventEmitter.fire(new InternalModelContentChangeEvent(rawChange, change)); } @@ -513,8 +461,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._increaseVersionId(); // Flush all tokens - this._tokens.flush(); - this._semanticTokens.flush(); + this._tokenizationTextModelPart.flush(); // Destroy all my decorations this._decorations = Object.create(null); @@ -600,7 +547,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati public onBeforeAttached(): void { this._attachedEditorCount++; if (this._attachedEditorCount === 1) { - this._tokenization.handleDidChangeAttached(); + this._tokenizationTextModelPart.handleDidChangeAttached(); this._onDidChangeAttached.fire(undefined); } } @@ -608,7 +555,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati public onBeforeDetached(): void { this._attachedEditorCount--; if (this._attachedEditorCount === 0) { - this._tokenization.handleDidChangeAttached(); + this._tokenizationTextModelPart.handleDidChangeAttached(); this._onDidChangeAttached.fire(undefined); } } @@ -697,7 +644,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const e = this._options.createChangeEvent(newOpts); this._options = newOpts; - this._bracketPairColorizer.handleDidChangeOptions(e); + this._bracketPairs.handleDidChangeOptions(e); this._decorationProvider.handleDidChangeOptions(e); this._onDidChangeOptions.fire(e); } @@ -1463,8 +1410,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati for (let i = 0, len = contentChanges.length; i < len; i++) { const change = contentChanges[i]; const [eolCount, firstLineLength, lastLineLength] = countEOL(change.text); - this._tokens.acceptEdit(change.range, eolCount, firstLineLength); - this._semanticTokens.acceptEdit(change.range, eolCount, firstLineLength, lastLineLength, change.text.length > 0 ? change.text.charCodeAt(0) : CharCode.Null); + this._tokenizationTextModelPart.acceptEdit(change.range, change.text, eolCount, firstLineLength, lastLineLength); this._decorationsTree.acceptReplace(change.rangeOffset, change.rangeLength, change.text.length, change.forceMoveMarkers); } @@ -1944,270 +1890,20 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati //#region Tokenization - public setLineTokens(lineNumber: number, tokens: Uint32Array | ArrayBuffer | null): void { - if (lineNumber < 1 || lineNumber > this.getLineCount()) { - throw new Error('Illegal value for lineNumber'); - } - - this._tokens.setTokens(this._languageId, lineNumber - 1, this._buffer.getLineLength(lineNumber), tokens, false); - } - - public setTokens(tokens: ContiguousMultilineTokens[], backgroundTokenizationCompleted: boolean = false): void { - if (tokens.length !== 0) { - const ranges: { fromLineNumber: number; toLineNumber: number }[] = []; - - for (let i = 0, len = tokens.length; i < len; i++) { - const element = tokens[i]; - let minChangedLineNumber = 0; - let maxChangedLineNumber = 0; - let hasChange = false; - for (let lineNumber = element.startLineNumber; lineNumber <= element.endLineNumber; lineNumber++) { - if (hasChange) { - this._tokens.setTokens(this._languageId, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.getLineTokens(lineNumber), false); - maxChangedLineNumber = lineNumber; - } else { - const lineHasChange = this._tokens.setTokens(this._languageId, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.getLineTokens(lineNumber), true); - if (lineHasChange) { - hasChange = true; - minChangedLineNumber = lineNumber; - maxChangedLineNumber = lineNumber; - } - } - } - if (hasChange) { - ranges.push({ fromLineNumber: minChangedLineNumber, toLineNumber: maxChangedLineNumber }); - } - } - - if (ranges.length > 0) { - this._emitModelTokensChangedEvent({ - tokenizationSupportChanged: false, - semanticTokensApplied: false, - ranges: ranges - }); - } - } - this.handleTokenizationProgress(backgroundTokenizationCompleted); - } - - public setSemanticTokens(tokens: SparseMultilineTokens[] | null, isComplete: boolean): void { - this._semanticTokens.set(tokens, isComplete); - - this._emitModelTokensChangedEvent({ - tokenizationSupportChanged: false, - semanticTokensApplied: tokens !== null, - ranges: [{ fromLineNumber: 1, toLineNumber: this.getLineCount() }] - }); - } - - public hasCompleteSemanticTokens(): boolean { - return this._semanticTokens.isComplete(); - } - - public hasSomeSemanticTokens(): boolean { - return !this._semanticTokens.isEmpty(); - } - - public setPartialSemanticTokens(range: Range, tokens: SparseMultilineTokens[]): void { - if (this.hasCompleteSemanticTokens()) { - return; - } - const changedRange = this.validateRange(this._semanticTokens.setPartial(range, tokens)); - - this._emitModelTokensChangedEvent({ - tokenizationSupportChanged: false, - semanticTokensApplied: true, - ranges: [{ fromLineNumber: changedRange.startLineNumber, toLineNumber: changedRange.endLineNumber }] - }); - } - - public tokenizeViewport(startLineNumber: number, endLineNumber: number): void { - startLineNumber = Math.max(1, startLineNumber); - endLineNumber = Math.min(this._buffer.getLineCount(), endLineNumber); - this._tokenization.tokenizeViewport(startLineNumber, endLineNumber); - } - - public clearTokens(): void { - this._tokens.flush(); - this._emitModelTokensChangedEvent({ - tokenizationSupportChanged: true, - semanticTokensApplied: false, - ranges: [{ - fromLineNumber: 1, - toLineNumber: this._buffer.getLineCount() - }] - }); - } - - public clearSemanticTokens(): void { - this._semanticTokens.flush(); - - this._emitModelTokensChangedEvent({ - tokenizationSupportChanged: false, - semanticTokensApplied: false, - ranges: [{ fromLineNumber: 1, toLineNumber: this.getLineCount() }] - }); - } - - private _emitModelTokensChangedEvent(e: IModelTokensChangedEvent): void { - if (!this._isDisposing) { - this._bracketPairColorizer.handleDidChangeTokens(e); - this._onDidChangeTokens.fire(e); - } - } - - public resetTokenization(): void { - this._tokenization.reset(); - } - - public forceTokenization(lineNumber: number): void { - if (lineNumber < 1 || lineNumber > this.getLineCount()) { - throw new Error('Illegal value for lineNumber'); - } - - this._tokenization.forceTokenization(lineNumber); - } - - public isCheapToTokenize(lineNumber: number): boolean { - return this._tokenization.isCheapToTokenize(lineNumber); - } - - public tokenizeIfCheap(lineNumber: number): void { - if (this.isCheapToTokenize(lineNumber)) { - this.forceTokenization(lineNumber); - } - } - - public getLineTokens(lineNumber: number): LineTokens { - if (lineNumber < 1 || lineNumber > this.getLineCount()) { - throw new Error('Illegal value for lineNumber'); - } - - return this._getLineTokens(lineNumber); - } - - private _getLineTokens(lineNumber: number): LineTokens { - const lineText = this.getLineContent(lineNumber); - const syntacticTokens = this._tokens.getTokens(this._languageId, lineNumber - 1, lineText); - return this._semanticTokens.addSparseTokens(lineNumber, syntacticTokens); - } - + // TODO move them to the tokenization part. public getLanguageId(): string { - return this._languageId; + return this.tokenization.getLanguageId(); } public setMode(languageId: string): void { - if (this._languageId === languageId) { - // There's nothing to do - return; - } - - const e: IModelLanguageChangedEvent = { - oldLanguage: this._languageId, - newLanguage: languageId - }; - - this._languageId = languageId; - - this._bracketPairColorizer.handleDidChangeLanguage(e); - this._tokenization.handleDidChangeLanguage(e); - this._onDidChangeLanguage.fire(e); - this._onDidChangeLanguageConfiguration.fire({}); + this.tokenization.setLanguageId(languageId); } public getLanguageIdAtPosition(lineNumber: number, column: number): string { - const position = this.validatePosition(new Position(lineNumber, column)); - const lineTokens = this.getLineTokens(position.lineNumber); - return lineTokens.getLanguageId(lineTokens.findTokenIndexAtOffset(position.column - 1)); - } - - public getTokenTypeIfInsertingCharacter(lineNumber: number, column: number, character: string): StandardTokenType { - const position = this.validatePosition(new Position(lineNumber, column)); - return this._tokenization.getTokenTypeIfInsertingCharacter(position, character); - } - - tokenizeLineWithEdit(position: IPosition, length: number, newText: string): LineTokens | null { - const validatedPosition = this.validatePosition(position); - return this._tokenization.tokenizeLineWithEdit(validatedPosition, length, newText); - } - - private getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration { - return this._languageConfigurationService.getLanguageConfiguration(languageId); - } - - // Having tokens allows implementing additional helper methods - - public getWordAtPosition(_position: IPosition): IWordAtPosition | null { - this._assertNotDisposed(); - const position = this.validatePosition(_position); - const lineContent = this.getLineContent(position.lineNumber); - const lineTokens = this._getLineTokens(position.lineNumber); - const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); - - // (1). First try checking right biased word - const [rbStartOffset, rbEndOffset] = TextModel._findLanguageBoundaries(lineTokens, tokenIndex); - const rightBiasedWord = getWordAtText( - position.column, - this.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex)).getWordDefinition(), - lineContent.substring(rbStartOffset, rbEndOffset), - rbStartOffset - ); - // Make sure the result touches the original passed in position - if (rightBiasedWord && rightBiasedWord.startColumn <= _position.column && _position.column <= rightBiasedWord.endColumn) { - return rightBiasedWord; - } - - // (2). Else, if we were at a language boundary, check the left biased word - if (tokenIndex > 0 && rbStartOffset === position.column - 1) { - // edge case, where `position` sits between two tokens belonging to two different languages - const [lbStartOffset, lbEndOffset] = TextModel._findLanguageBoundaries(lineTokens, tokenIndex - 1); - const leftBiasedWord = getWordAtText( - position.column, - this.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex - 1)).getWordDefinition(), - lineContent.substring(lbStartOffset, lbEndOffset), - lbStartOffset - ); - // Make sure the result touches the original passed in position - if (leftBiasedWord && leftBiasedWord.startColumn <= _position.column && _position.column <= leftBiasedWord.endColumn) { - return leftBiasedWord; - } - } - - return null; + return this.tokenization.getLanguageIdAtPosition(lineNumber, column); } - - private static _findLanguageBoundaries(lineTokens: LineTokens, tokenIndex: number): [number, number] { - const languageId = lineTokens.getLanguageId(tokenIndex); - - // go left until a different language is hit - let startOffset = 0; - for (let i = tokenIndex; i >= 0 && lineTokens.getLanguageId(i) === languageId; i--) { - startOffset = lineTokens.getStartOffset(i); - } - - // go right until a different language is hit - let endOffset = lineTokens.getLineContent().length; - for (let i = tokenIndex, tokenCount = lineTokens.getCount(); i < tokenCount && lineTokens.getLanguageId(i) === languageId; i++) { - endOffset = lineTokens.getEndOffset(i); - } - - return [startOffset, endOffset]; - } - - public getWordUntilPosition(position: IPosition): IWordAtPosition { - const wordAtPosition = this.getWordAtPosition(position); - if (!wordAtPosition) { - return { - word: '', - startColumn: position.column, - endColumn: position.column - }; - } - return { - word: wordAtPosition.word.substr(0, position.column - wordAtPosition.startColumn), - startColumn: wordAtPosition.startColumn, - endColumn: position.column - }; + public setLineTokens(lineNumber: number, tokens: Uint32Array | ArrayBuffer | null): void { + this._tokenizationTextModelPart.setLineTokens(lineNumber, tokens); } //#endregion diff --git a/src/vs/editor/common/model/textModelPart.ts b/src/vs/editor/common/model/textModelPart.ts index 7a37fda1f3a..ef061c2ba9c 100644 --- a/src/vs/editor/common/model/textModelPart.ts +++ b/src/vs/editor/common/model/textModelPart.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; -export class TextModelPart implements IDisposable { +export class TextModelPart extends Disposable { private _isDisposed = false; - public dispose(): void { + public override dispose(): void { + super.dispose(); this._isDisposed = true; } protected assertNotDisposed(): void { diff --git a/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index 89accddef32..a0aaf377690 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/src/vs/editor/common/model/textModelTokens.ts @@ -18,6 +18,7 @@ import { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contig import { runWhenIdle, IdleDeadline } from 'vs/base/common/async'; import { setTimeout0 } from 'vs/base/common/platform'; import { IModelContentChangedEvent, IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents'; +import { TokenizationTextModelPart } from 'vs/editor/common/model/tokenizationTextModelPart'; const enum Constants { CHEAP_TOKENIZATION_LENGTH_LIMIT = 2048 @@ -166,6 +167,7 @@ export class TextModelTokenization extends Disposable { constructor( private readonly _textModel: TextModel, + private readonly _tokenizationPart: TokenizationTextModelPart, private readonly _languageIdCodec: ILanguageIdCodec ) { super(); @@ -179,7 +181,7 @@ export class TextModelTokenization extends Disposable { } this._resetTokenizationState(); - this._textModel.clearTokens(); + this._tokenizationPart.clearTokens(); })); this._resetTokenizationState(); @@ -214,13 +216,13 @@ export class TextModelTokenization extends Disposable { public handleDidChangeLanguage(e: IModelLanguageChangedEvent): void { this._resetTokenizationState(); - this._textModel.clearTokens(); + this._tokenizationPart.clearTokens(); } //#endregion private _resetTokenizationState(): void { - const [tokenizationSupport, initialState] = initializeTokenization(this._textModel); + const [tokenizationSupport, initialState] = initializeTokenization(this._textModel, this._tokenizationPart); if (tokenizationSupport && initialState) { this._tokenizationStateStore = new TokenizationStateStore(tokenizationSupport, initialState); } else { @@ -294,24 +296,24 @@ export class TextModelTokenization extends Disposable { } } while (this._hasLinesToTokenize()); - this._textModel.setTokens(builder.finalize(), this._isTokenizationComplete()); + this._tokenizationPart.setTokens(builder.finalize(), this._isTokenizationComplete()); } public tokenizeViewport(startLineNumber: number, endLineNumber: number): void { const builder = new ContiguousMultilineTokensBuilder(); this._tokenizeViewport(builder, startLineNumber, endLineNumber); - this._textModel.setTokens(builder.finalize(), this._isTokenizationComplete()); + this._tokenizationPart.setTokens(builder.finalize(), this._isTokenizationComplete()); } public reset(): void { this._resetTokenizationState(); - this._textModel.clearTokens(); + this._tokenizationPart.clearTokens(); } public forceTokenization(lineNumber: number): void { const builder = new ContiguousMultilineTokensBuilder(); this._updateTokensUntilLine(builder, lineNumber); - this._textModel.setTokens(builder.finalize(), this._isTokenizationComplete()); + this._tokenizationPart.setTokens(builder.finalize(), this._isTokenizationComplete()); } public getTokenTypeIfInsertingCharacter(position: Position, character: string): StandardTokenType { @@ -499,11 +501,11 @@ export class TextModelTokenization extends Disposable { } } -function initializeTokenization(textModel: TextModel): [ITokenizationSupport, IState] | [null, null] { +function initializeTokenization(textModel: TextModel, tokenizationPart: TokenizationTextModelPart): [ITokenizationSupport, IState] | [null, null] { if (textModel.isTooLargeForTokenization()) { return [null, null]; } - const tokenizationSupport = TokenizationRegistry.get(textModel.getLanguageId()); + const tokenizationSupport = TokenizationRegistry.get(tokenizationPart.getLanguageId()); if (!tokenizationSupport) { return [null, null]; } diff --git a/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts new file mode 100644 index 00000000000..e873766cfe4 --- /dev/null +++ b/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -0,0 +1,610 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { CharCode } from 'vs/base/common/charCode'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { getWordAtText, IWordAtPosition } from 'vs/editor/common/core/wordHelper'; +import { StandardTokenType } from 'vs/editor/common/languages'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { TextModelPart } from 'vs/editor/common/model/textModelPart'; +import { TextModelTokenization } from 'vs/editor/common/model/textModelTokens'; +import { IModelContentChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents'; +import { ContiguousMultilineTokens } from 'vs/editor/common/tokens/contiguousMultilineTokens'; +import { ContiguousTokensStore } from 'vs/editor/common/tokens/contiguousTokensStore'; +import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens'; +import { SparseTokensStore } from 'vs/editor/common/tokens/sparseTokensStore'; +import { BracketPairsTextModelPart } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl'; + +export interface ITokenizationTextModelPart { + /** + * @internal + */ + setTokens(tokens: ContiguousMultilineTokens[]): void; + + /** + * @internal + */ + setSemanticTokens(tokens: SparseMultilineTokens[] | null, isComplete: boolean): void; + + /** + * @internal + */ + setPartialSemanticTokens(range: Range, tokens: SparseMultilineTokens[] | null): void; + + /** + * @internal + */ + hasCompleteSemanticTokens(): boolean; + + /** + * @internal + */ + hasSomeSemanticTokens(): boolean; + + /** + * Flush all tokenization state. + * @internal + */ + resetTokenization(): void; + + /** + * Force tokenization information for `lineNumber` to be accurate. + * @internal + */ + forceTokenization(lineNumber: number): void; + + /** + * If it is cheap, force tokenization information for `lineNumber` to be accurate. + * This is based on a heuristic. + * @internal + */ + tokenizeIfCheap(lineNumber: number): void; + + /** + * Check if calling `forceTokenization` for this `lineNumber` will be cheap (time-wise). + * This is based on a heuristic. + * @internal + */ + isCheapToTokenize(lineNumber: number): boolean; + + /** + * Get the tokens for the line `lineNumber`. + * The tokens might be inaccurate. Use `forceTokenization` to ensure accurate tokens. + * @internal + */ + getLineTokens(lineNumber: number): LineTokens; + + /** + * Returns the standard token type for a character if the character were to be inserted at + * the given position. If the result cannot be accurate, it returns null. + * @internal + */ + getTokenTypeIfInsertingCharacter(lineNumber: number, column: number, character: string): StandardTokenType; + + /** + * @internal + */ + tokenizeLineWithEdit(position: IPosition, length: number, newText: string): LineTokens | null; + + /** + * Get the word under or besides `position`. + * @param position The position to look for a word. + * @return The word under or besides `position`. Might be null. + */ + getWordAtPosition(position: IPosition): IWordAtPosition | null; + + /** + * Get the word under or besides `position` trimmed to `position`.column + * @param position The position to look for a word. + * @return The word under or besides `position`. Will never be null. + */ + getWordUntilPosition(position: IPosition): IWordAtPosition; + + /** + * @internal + */ + tokenizeViewport(startLineNumber: number, endLineNumber: number): void; + + getLanguageId(): string; + getLanguageIdAtPosition(lineNumber: number, column: number): string; + + setLanguageId(languageId: string): void; + + readonly backgroundTokenizationState: BackgroundTokenizationState; + readonly onBackgroundTokenizationStateChanged: Event; +} + +export const enum BackgroundTokenizationState { + Uninitialized = 0, + InProgress = 1, + Completed = 2, +} + +export class TokenizationTextModelPart extends TextModelPart implements ITokenizationTextModelPart { + private readonly _onDidChangeLanguage: Emitter = this._register(new Emitter()); + public readonly onDidChangeLanguage: Event = this._onDidChangeLanguage.event; + + private readonly _onDidChangeLanguageConfiguration: Emitter = this._register(new Emitter()); + public readonly onDidChangeLanguageConfiguration: Event = this._onDidChangeLanguageConfiguration.event; + + private readonly _onDidChangeTokens: Emitter = this._register(new Emitter()); + public readonly onDidChangeTokens: Event = this._onDidChangeTokens.event; + + private readonly _languageRegistryListener: IDisposable; + private readonly _tokens: ContiguousTokensStore; + private readonly _semanticTokens: SparseTokensStore; + private readonly _tokenization: TextModelTokenization; + + constructor( + private readonly _languageService: ILanguageService, + private readonly _languageConfigurationService: ILanguageConfigurationService, + private readonly _textModel: TextModel, + private readonly bracketPairsTextModelPart: BracketPairsTextModelPart, + private _languageId: string, + ) { + super(); + + this._tokens = new ContiguousTokensStore( + this._languageService.languageIdCodec + ); + this._semanticTokens = new SparseTokensStore( + this._languageService.languageIdCodec + ); + this._tokenization = new TextModelTokenization( + _textModel, + this, + this._languageService.languageIdCodec + ); + + this._languageRegistryListener = this._languageConfigurationService.onDidChange( + e => { + if (e.affects(this._languageId)) { + this._onDidChangeLanguageConfiguration.fire({}); + } + } + ); + } + + _hasListeners(): boolean { + return ( + this._onDidChangeLanguage.hasListeners() + || this._onDidChangeLanguageConfiguration.hasListeners() + || this._onDidChangeTokens.hasListeners() + || this._onBackgroundTokenizationStateChanged.hasListeners() + ); + } + + public acceptEdit( + range: IRange, + text: string, + eolCount: number, + firstLineLength: number, + lastLineLength: number + ): void { + this._tokens.acceptEdit(range, eolCount, firstLineLength); + this._semanticTokens.acceptEdit( + range, + eolCount, + firstLineLength, + lastLineLength, + text.length > 0 ? text.charCodeAt(0) : CharCode.Null + ); + } + + public handleDidChangeAttached(): void { + this._tokenization.handleDidChangeAttached(); + } + + public flush(): void { + this._tokens.flush(); + this._semanticTokens.flush(); + } + + public handleDidChangeContent(change: IModelContentChangedEvent): void { + this._tokenization.handleDidChangeContent(change); + } + + public override dispose(): void { + this._languageRegistryListener.dispose(); + this._tokenization.dispose(); + super.dispose(); + } + + private _backgroundTokenizationState = BackgroundTokenizationState.Uninitialized; + public get backgroundTokenizationState(): BackgroundTokenizationState { + return this._backgroundTokenizationState; + } + private handleTokenizationProgress(completed: boolean) { + if (this._backgroundTokenizationState === BackgroundTokenizationState.Completed) { + // We already did a full tokenization and don't go back to progressing. + return; + } + const newState = completed ? BackgroundTokenizationState.Completed : BackgroundTokenizationState.InProgress; + if (this._backgroundTokenizationState !== newState) { + this._backgroundTokenizationState = newState; + this.bracketPairsTextModelPart.handleDidChangeBackgroundTokenizationState(); + this._onBackgroundTokenizationStateChanged.fire(); + } + } + + private readonly _onBackgroundTokenizationStateChanged = this._register(new Emitter()); + public readonly onBackgroundTokenizationStateChanged: Event = this._onBackgroundTokenizationStateChanged.event; + + public setLineTokens( + lineNumber: number, + tokens: Uint32Array | ArrayBuffer | null + ): void { + if (lineNumber < 1 || lineNumber > this._textModel.getLineCount()) { + throw new Error('Illegal value for lineNumber'); + } + + this._tokens.setTokens( + this._languageId, + lineNumber - 1, + this._textModel.getLineLength(lineNumber), + tokens, + false + ); + } + + public setTokens( + tokens: ContiguousMultilineTokens[], + backgroundTokenizationCompleted: boolean = false + ): void { + if (tokens.length !== 0) { + const ranges: { fromLineNumber: number; toLineNumber: number }[] = []; + + for (let i = 0, len = tokens.length; i < len; i++) { + const element = tokens[i]; + let minChangedLineNumber = 0; + let maxChangedLineNumber = 0; + let hasChange = false; + for ( + let lineNumber = element.startLineNumber; + lineNumber <= element.endLineNumber; + lineNumber++ + ) { + if (hasChange) { + this._tokens.setTokens( + this._languageId, + lineNumber - 1, + this._textModel.getLineLength(lineNumber), + element.getLineTokens(lineNumber), + false + ); + maxChangedLineNumber = lineNumber; + } else { + const lineHasChange = this._tokens.setTokens( + this._languageId, + lineNumber - 1, + this._textModel.getLineLength(lineNumber), + element.getLineTokens(lineNumber), + true + ); + if (lineHasChange) { + hasChange = true; + minChangedLineNumber = lineNumber; + maxChangedLineNumber = lineNumber; + } + } + } + if (hasChange) { + ranges.push({ + fromLineNumber: minChangedLineNumber, + toLineNumber: maxChangedLineNumber, + }); + } + } + + if (ranges.length > 0) { + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: false, + semanticTokensApplied: false, + ranges: ranges, + }); + } + } + this.handleTokenizationProgress(backgroundTokenizationCompleted); + } + + public setSemanticTokens( + tokens: SparseMultilineTokens[] | null, + isComplete: boolean + ): void { + this._semanticTokens.set(tokens, isComplete); + + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: false, + semanticTokensApplied: tokens !== null, + ranges: [{ fromLineNumber: 1, toLineNumber: this._textModel.getLineCount() }], + }); + } + + public hasCompleteSemanticTokens(): boolean { + return this._semanticTokens.isComplete(); + } + + public hasSomeSemanticTokens(): boolean { + return !this._semanticTokens.isEmpty(); + } + + public setPartialSemanticTokens( + range: Range, + tokens: SparseMultilineTokens[] + ): void { + if (this.hasCompleteSemanticTokens()) { + return; + } + const changedRange = this._textModel.validateRange( + this._semanticTokens.setPartial(range, tokens) + ); + + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: false, + semanticTokensApplied: true, + ranges: [ + { + fromLineNumber: changedRange.startLineNumber, + toLineNumber: changedRange.endLineNumber, + }, + ], + }); + } + + public tokenizeViewport( + startLineNumber: number, + endLineNumber: number + ): void { + startLineNumber = Math.max(1, startLineNumber); + endLineNumber = Math.min(this._textModel.getLineCount(), endLineNumber); + this._tokenization.tokenizeViewport(startLineNumber, endLineNumber); + } + + public clearTokens(): void { + this._tokens.flush(); + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: true, + semanticTokensApplied: false, + ranges: [ + { + fromLineNumber: 1, + toLineNumber: this._textModel.getLineCount(), + }, + ], + }); + } + + public clearSemanticTokens(): void { + this._semanticTokens.flush(); + + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: false, + semanticTokensApplied: false, + ranges: [{ fromLineNumber: 1, toLineNumber: this._textModel.getLineCount() }], + }); + } + + private _emitModelTokensChangedEvent(e: IModelTokensChangedEvent): void { + if (!this._textModel._isDisposing) { + this.bracketPairsTextModelPart.handleDidChangeTokens(e); + this._onDidChangeTokens.fire(e); + } + } + + public resetTokenization(): void { + this._tokenization.reset(); + } + + public forceTokenization(lineNumber: number): void { + if (lineNumber < 1 || lineNumber > this._textModel.getLineCount()) { + throw new Error('Illegal value for lineNumber'); + } + + this._tokenization.forceTokenization(lineNumber); + } + + public isCheapToTokenize(lineNumber: number): boolean { + return this._tokenization.isCheapToTokenize(lineNumber); + } + + public tokenizeIfCheap(lineNumber: number): void { + if (this.isCheapToTokenize(lineNumber)) { + this.forceTokenization(lineNumber); + } + } + + public getLineTokens(lineNumber: number): LineTokens { + if (lineNumber < 1 || lineNumber > this._textModel.getLineCount()) { + throw new Error('Illegal value for lineNumber'); + } + + return this._getLineTokens(lineNumber); + } + + private _getLineTokens(lineNumber: number): LineTokens { + const lineText = this._textModel.getLineContent(lineNumber); + const syntacticTokens = this._tokens.getTokens( + this._languageId, + lineNumber - 1, + lineText + ); + return this._semanticTokens.addSparseTokens(lineNumber, syntacticTokens); + } + + public getTokenTypeIfInsertingCharacter( + lineNumber: number, + column: number, + character: string + ): StandardTokenType { + const position = this._textModel.validatePosition(new Position(lineNumber, column)); + return this._tokenization.getTokenTypeIfInsertingCharacter( + position, + character + ); + } + + public tokenizeLineWithEdit( + position: IPosition, + length: number, + newText: string + ): LineTokens | null { + const validatedPosition = this._textModel.validatePosition(position); + return this._tokenization.tokenizeLineWithEdit( + validatedPosition, + length, + newText + ); + } + + private getLanguageConfiguration( + languageId: string + ): ResolvedLanguageConfiguration { + return this._languageConfigurationService.getLanguageConfiguration( + languageId + ); + } + + // Having tokens allows implementing additional helper methods + + public getWordAtPosition(_position: IPosition): IWordAtPosition | null { + this.assertNotDisposed(); + const position = this._textModel.validatePosition(_position); + const lineContent = this._textModel.getLineContent(position.lineNumber); + const lineTokens = this._getLineTokens(position.lineNumber); + const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + + // (1). First try checking right biased word + const [rbStartOffset, rbEndOffset] = TokenizationTextModelPart._findLanguageBoundaries( + lineTokens, + tokenIndex + ); + const rightBiasedWord = getWordAtText( + position.column, + this.getLanguageConfiguration( + lineTokens.getLanguageId(tokenIndex) + ).getWordDefinition(), + lineContent.substring(rbStartOffset, rbEndOffset), + rbStartOffset + ); + // Make sure the result touches the original passed in position + if ( + rightBiasedWord && + rightBiasedWord.startColumn <= _position.column && + _position.column <= rightBiasedWord.endColumn + ) { + return rightBiasedWord; + } + + // (2). Else, if we were at a language boundary, check the left biased word + if (tokenIndex > 0 && rbStartOffset === position.column - 1) { + // edge case, where `position` sits between two tokens belonging to two different languages + const [lbStartOffset, lbEndOffset] = TokenizationTextModelPart._findLanguageBoundaries( + lineTokens, + tokenIndex - 1 + ); + const leftBiasedWord = getWordAtText( + position.column, + this.getLanguageConfiguration( + lineTokens.getLanguageId(tokenIndex - 1) + ).getWordDefinition(), + lineContent.substring(lbStartOffset, lbEndOffset), + lbStartOffset + ); + // Make sure the result touches the original passed in position + if ( + leftBiasedWord && + leftBiasedWord.startColumn <= _position.column && + _position.column <= leftBiasedWord.endColumn + ) { + return leftBiasedWord; + } + } + + return null; + } + + private static _findLanguageBoundaries( + lineTokens: LineTokens, + tokenIndex: number + ): [number, number] { + const languageId = lineTokens.getLanguageId(tokenIndex); + + // go left until a different language is hit + let startOffset = 0; + for ( + let i = tokenIndex; + i >= 0 && lineTokens.getLanguageId(i) === languageId; + i-- + ) { + startOffset = lineTokens.getStartOffset(i); + } + + // go right until a different language is hit + let endOffset = lineTokens.getLineContent().length; + for ( + let i = tokenIndex, tokenCount = lineTokens.getCount(); + i < tokenCount && lineTokens.getLanguageId(i) === languageId; + i++ + ) { + endOffset = lineTokens.getEndOffset(i); + } + + return [startOffset, endOffset]; + } + + public getWordUntilPosition(position: IPosition): IWordAtPosition { + const wordAtPosition = this.getWordAtPosition(position); + if (!wordAtPosition) { + return { + word: '', + startColumn: position.column, + endColumn: position.column, + }; + } + return { + word: wordAtPosition.word.substr( + 0, + position.column - wordAtPosition.startColumn + ), + startColumn: wordAtPosition.startColumn, + endColumn: position.column, + }; + } + + public getLanguageId(): string { + return this._languageId; + } + + public getLanguageIdAtPosition(lineNumber: number, column: number): string { + const position = this._textModel.validatePosition(new Position(lineNumber, column)); + const lineTokens = this.getLineTokens(position.lineNumber); + return lineTokens.getLanguageId(lineTokens.findTokenIndexAtOffset(position.column - 1)); + } + + public setLanguageId(languageId: string): void { + if (this._languageId === languageId) { + // There's nothing to do + return; + } + + const e: IModelLanguageChangedEvent = { + oldLanguage: this._languageId, + newLanguage: languageId + }; + + this._languageId = languageId; + + this.bracketPairsTextModelPart.handleDidChangeLanguage(e); + this._tokenization.handleDidChangeLanguage(e); + this._onDidChangeLanguage.fire(e); + this._onDidChangeLanguageConfiguration.fire({}); + } +} diff --git a/src/vs/editor/common/services/markerDecorationsService.ts b/src/vs/editor/common/services/markerDecorationsService.ts index bd67f6e1bfe..98bd1dffbc4 100644 --- a/src/vs/editor/common/services/markerDecorationsService.ts +++ b/src/vs/editor/common/services/markerDecorationsService.ts @@ -163,7 +163,7 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor return ret; } - const word = model.getWordAtPosition(ret.getStartPosition()); + const word = model.tokenization.getWordAtPosition(ret.getStartPosition()); if (word) { ret = new Range(ret.startLineNumber, word.startColumn, ret.endLineNumber, word.endColumn); } diff --git a/src/vs/editor/common/services/modelService.ts b/src/vs/editor/common/services/modelService.ts index 6f6e79ee9a5..cdcd12ecfa2 100644 --- a/src/vs/editor/common/services/modelService.ts +++ b/src/vs/editor/common/services/modelService.ts @@ -850,7 +850,7 @@ export class ModelSemanticColoring extends Disposable { // there is no provider if (this._currentDocumentResponse) { // there are semantic tokens set - this._model.setSemanticTokens(null, false); + this._model.tokenization.setSemanticTokens(null, false); } return; } @@ -925,11 +925,11 @@ export class ModelSemanticColoring extends Disposable { return; } if (!provider || !styling) { - this._model.setSemanticTokens(null, false); + this._model.tokenization.setSemanticTokens(null, false); return; } if (!tokens) { - this._model.setSemanticTokens(null, true); + this._model.tokenization.setSemanticTokens(null, true); rescheduleIfNeeded(); return; } @@ -937,7 +937,7 @@ export class ModelSemanticColoring extends Disposable { if (isSemanticTokensEdits(tokens)) { if (!currentResponse) { // not possible! - this._model.setSemanticTokens(null, true); + this._model.tokenization.setSemanticTokens(null, true); return; } if (tokens.edits.length === 0) { @@ -1006,9 +1006,9 @@ export class ModelSemanticColoring extends Disposable { } } - this._model.setSemanticTokens(result, true); + this._model.tokenization.setSemanticTokens(result, true); } else { - this._model.setSemanticTokens(null, true); + this._model.tokenization.setSemanticTokens(null, true); } rescheduleIfNeeded(); diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 265cb543054..6e6dade760e 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -15,6 +15,12 @@ export enum AccessibilitySupport { Enabled = 2 } +export enum BackgroundTokenizationState { + Uninitialized = 0, + InProgress = 1, + Completed = 2 +} + export enum CompletionItemInsertTextRule { /** * Adjust whitespace/indentation of multiline insert texts to diff --git a/src/vs/editor/common/viewModel/modelLineProjection.ts b/src/vs/editor/common/viewModel/modelLineProjection.ts index 8a2380d3b74..a43c41fc870 100644 --- a/src/vs/editor/common/viewModel/modelLineProjection.ts +++ b/src/vs/editor/common/viewModel/modelLineProjection.ts @@ -37,7 +37,9 @@ export interface IModelLineProjection { } export interface ISimpleModel { - getLineTokens(lineNumber: number): LineTokens; + tokenization: { + getLineTokens(lineNumber: number): LineTokens; + }; getLineContent(lineNumber: number): string; getLineLength(lineNumber: number): number; getLineMinColumn(lineNumber: number): number; @@ -211,13 +213,13 @@ class ModelLineProjection implements IModelLineProjection { let lineWithInjections: LineTokens; if (injectionOffsets) { - lineWithInjections = model.getLineTokens(modelLineNumber).withInserted(injectionOffsets.map((offset, idx) => ({ + lineWithInjections = model.tokenization.getLineTokens(modelLineNumber).withInserted(injectionOffsets.map((offset, idx) => ({ offset, text: injectionOptions![idx].content, tokenMetadata: LineTokens.defaultTokenMetadata }))); } else { - lineWithInjections = model.getLineTokens(modelLineNumber); + lineWithInjections = model.tokenization.getLineTokens(modelLineNumber); } for (let outputLineIndex = outputLineIdx; outputLineIndex < outputLineIdx + lineCount; outputLineIndex++) { @@ -339,7 +341,7 @@ class IdentityModelLineProjection implements IModelLineProjection { } public getViewLineData(model: ISimpleModel, modelLineNumber: number, _outputLineIndex: number): ViewLineData { - const lineTokens = model.getLineTokens(modelLineNumber); + const lineTokens = model.tokenization.getLineTokens(modelLineNumber); const lineContent = lineTokens.getLineContent(); return new ViewLineData( lineContent, diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index 47d1d23ea99..e9db60465a0 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -204,7 +204,7 @@ export function isModelDecorationInString(model: ITextModel, decoration: IModelD */ function testTokensInRange(model: ITextModel, range: Range, callback: (tokenType: StandardTokenType) => boolean): boolean { for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) { - const lineTokens = model.getLineTokens(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); const isFirstLine = lineNumber === range.startLineNumber; const isEndLine = lineNumber === range.endLineNumber; diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index c6ab57b1186..066b3bef4ce 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -193,7 +193,7 @@ export class ViewModel extends Disposable implements IViewModel { const modelVisibleRanges = this._toModelVisibleRanges(viewVisibleRange); for (const modelVisibleRange of modelVisibleRanges) { - this.model.tokenizeViewport(modelVisibleRange.startLineNumber, modelVisibleRange.endLineNumber); + this.model.tokenization.tokenizeViewport(modelVisibleRange.startLineNumber, modelVisibleRange.endLineNumber); } } @@ -914,7 +914,7 @@ export class ViewModel extends Disposable implements IViewModel { let result = ''; for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { - const lineTokens = this.model.getLineTokens(lineNumber); + const lineTokens = this.model.tokenization.getLineTokens(lineNumber); const lineContent = lineTokens.getLineContent(); const startOffset = (lineNumber === startLineNumber ? startColumn - 1 : 0); const endOffset = (lineNumber === endLineNumber ? endColumn - 1 : lineContent.length); diff --git a/src/vs/editor/common/viewModel/viewModelLines.ts b/src/vs/editor/common/viewModel/viewModelLines.ts index 2dbfff15b9e..31d5e91de7f 100644 --- a/src/vs/editor/common/viewModel/viewModelLines.ts +++ b/src/vs/editor/common/viewModel/viewModelLines.ts @@ -1200,7 +1200,7 @@ export class ViewModelLinesFromModelAsIs implements IViewModelLines { } public getViewLineData(viewLineNumber: number): ViewLineData { - const lineTokens = this.model.getLineTokens(viewLineNumber); + const lineTokens = this.model.tokenization.getLineTokens(viewLineNumber); const lineContent = lineTokens.getLineContent(); return new ViewLineData( lineContent, diff --git a/src/vs/editor/contrib/comment/browser/blockCommentCommand.ts b/src/vs/editor/contrib/comment/browser/blockCommentCommand.ts index b0a162d3422..08c999bf42b 100644 --- a/src/vs/editor/contrib/comment/browser/blockCommentCommand.ts +++ b/src/vs/editor/contrib/comment/browser/blockCommentCommand.ts @@ -170,7 +170,7 @@ export class BlockCommentCommand implements ICommand { const startLineNumber = this._selection.startLineNumber; const startColumn = this._selection.startColumn; - model.tokenizeIfCheap(startLineNumber); + model.tokenization.tokenizeIfCheap(startLineNumber); const languageId = model.getLanguageIdAtPosition(startLineNumber, startColumn); const config = this.languageConfigurationService.getLanguageConfiguration(languageId).comments; if (!config || !config.blockCommentStartToken || !config.blockCommentEndToken) { diff --git a/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts b/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts index c1964f40201..97770268095 100644 --- a/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts +++ b/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts @@ -85,7 +85,7 @@ export class LineCommentCommand implements ICommand { */ private static _gatherPreflightCommentStrings(model: ITextModel, startLineNumber: number, endLineNumber: number, languageConfigurationService: ILanguageConfigurationService): ILinePreflightData[] | null { - model.tokenizeIfCheap(startLineNumber); + model.tokenization.tokenizeIfCheap(startLineNumber); const languageId = model.getLanguageIdAtPosition(startLineNumber, 1); const config = languageConfigurationService.getLanguageConfiguration(languageId).comments; @@ -282,7 +282,7 @@ export class LineCommentCommand implements ICommand { * Given an unsuccessful analysis, delegate to the block comment command */ private _executeBlockComment(model: ITextModel, builder: IEditOperationBuilder, s: Selection): void { - model.tokenizeIfCheap(s.startLineNumber); + model.tokenization.tokenizeIfCheap(s.startLineNumber); let languageId = model.getLanguageIdAtPosition(s.startLineNumber, 1); const config = this.languageConfigurationService.getLanguageConfiguration(languageId).comments; if (!config || !config.blockCommentStartToken || !config.blockCommentEndToken) { diff --git a/src/vs/editor/contrib/gotoError/browser/markerNavigationService.ts b/src/vs/editor/contrib/gotoError/browser/markerNavigationService.ts index b520f7fcb3f..5de4706d66a 100644 --- a/src/vs/editor/contrib/gotoError/browser/markerNavigationService.ts +++ b/src/vs/editor/contrib/gotoError/browser/markerNavigationService.ts @@ -117,7 +117,7 @@ export class MarkerList { let range = Range.lift(this._markers[i]); if (range.isEmpty()) { - const word = model.getWordAtPosition(range.getStartPosition()); + const word = model.tokenization.getWordAtPosition(range.getStartPosition()); if (word) { range = new Range(range.startLineNumber, word.startColumn, range.startLineNumber, word.endColumn); } diff --git a/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts b/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts index 75af03dba4c..748cd712393 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts @@ -129,7 +129,7 @@ export abstract class SymbolNavigationAction extends EditorAction { if (referenceCount === 0) { // no result -> show message if (!this.configuration.muteMessage) { - const info = model.getWordAtPosition(position); + const info = model.tokenization.getWordAtPosition(position); MessageController.get(editor)?.showMessage(this._getNoResultFoundMessage(info), position); } } else if (referenceCount === 1 && altAction) { diff --git a/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts b/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts index e6a7f84ee0d..3e32e4ac966 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts @@ -136,7 +136,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri this.toUnhookForKeyboard.clear(); // Find word at mouse position - const word = position ? this.editor.getModel()?.getWordAtPosition(position) : null; + const word = position ? this.editor.getModel()?.tokenization.getWordAtPosition(position) : null; if (!word) { this.currentWordAtPosition = null; this.removeLinkDecorations(); diff --git a/src/vs/editor/contrib/gotoSymbol/browser/referencesModel.ts b/src/vs/editor/contrib/gotoSymbol/browser/referencesModel.ts index 3fee138d351..0b3ddaa3ba4 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/referencesModel.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/referencesModel.ts @@ -81,7 +81,7 @@ export class FilePreview implements IDisposable { } const { startLineNumber, startColumn, endLineNumber, endColumn } = range; - const word = model.getWordUntilPosition({ lineNumber: startLineNumber, column: startColumn - n }); + const word = model.tokenization.getWordUntilPosition({ lineNumber: startLineNumber, column: startColumn - n }); const beforeRange = new Range(startLineNumber, word.startColumn, startLineNumber, startColumn); const afterRange = new Range(endLineNumber, endColumn, endLineNumber, Constants.MAX_SAFE_SMALL_INTEGER); diff --git a/src/vs/editor/contrib/indentation/browser/indentation.ts b/src/vs/editor/contrib/indentation/browser/indentation.ts index c7ea4ee4b3a..2f7bcfd1eae 100644 --- a/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -473,7 +473,7 @@ export class AutoIndentOnPaste implements IEditorContribution { return; } - if (!model.isCheapToTokenize(range.getStartPosition().lineNumber)) { + if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber)) { return; } const autoIndent = this.editor.getOption(EditorOption.autoIndent); @@ -546,14 +546,16 @@ export class AutoIndentOnPaste implements IEditorContribution { if (startLineNumber !== range.endLineNumber) { let virtualModel = { - getLineTokens: (lineNumber: number) => { - return model.getLineTokens(lineNumber); - }, - getLanguageId: () => { - return model.getLanguageId(); - }, - getLanguageIdAtPosition: (lineNumber: number, column: number) => { - return model.getLanguageIdAtPosition(lineNumber, column); + tokenization: { + getLineTokens: (lineNumber: number) => { + return model.tokenization.getLineTokens(lineNumber); + }, + getLanguageId: () => { + return model.getLanguageId(); + }, + getLanguageIdAtPosition: (lineNumber: number, column: number) => { + return model.getLanguageIdAtPosition(lineNumber, column); + }, }, getLineContent: (lineNumber: number) => { if (lineNumber === firstLineNumber) { @@ -597,12 +599,12 @@ export class AutoIndentOnPaste implements IEditorContribution { } private shouldIgnoreLine(model: ITextModel, lineNumber: number): boolean { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); let nonWhitespaceColumn = model.getLineFirstNonWhitespaceColumn(lineNumber); if (nonWhitespaceColumn === 0) { return true; } - let tokens = model.getLineTokens(lineNumber); + let tokens = model.tokenization.getLineTokens(lineNumber); if (tokens.getCount() > 0) { let firstNonWhitespaceTokenIndex = tokens.findTokenIndexAtOffset(nonWhitespaceColumn); if (firstNonWhitespaceTokenIndex >= 0 && tokens.getStandardTokenType(firstNonWhitespaceTokenIndex) === StandardTokenType.Comment) { diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHints.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHints.ts index a1f933965cc..5e65d98401d 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHints.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHints.ts @@ -133,14 +133,14 @@ export class InlayHintsFragments { private static _getRangeAtPosition(model: ITextModel, position: IPosition): Range { const line = position.lineNumber; - const word = model.getWordAtPosition(position); + const word = model.tokenization.getWordAtPosition(position); if (word) { // always prefer the word range return new Range(line, word.startColumn, line, word.endColumn); } - model.tokenizeIfCheap(line); - const tokens = model.getLineTokens(line); + model.tokenization.tokenizeIfCheap(line); + const tokens = model.tokenization.getLineTokens(line); const offset = position.column - 1; const idx = tokens.findTokenIndexAtOffset(offset); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 002b6262fcb..de7202fd15a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -771,7 +771,7 @@ export interface TrackedInlineCompletion extends NormalizedInlineCompletion { } function getDefaultRange(position: Position, model: ITextModel): Range { - const word = model.getWordAtPosition(position); + const word = model.tokenization.getWordAtPosition(position); const maxColumn = model.getLineMaxColumn(position.lineNumber); // By default, always replace up until the end of the current line. // This default might be subject to change! @@ -784,7 +784,7 @@ function closeBrackets(text: string, position: Position, model: ITextModel, lang const lineStart = model.getLineContent(position.lineNumber).substring(0, position.column - 1); const newLine = lineStart + text; - const newTokens = model.tokenizeLineWithEdit(position, newLine.length - (position.column - 1), text); + const newTokens = model.tokenization.tokenizeLineWithEdit(position, newLine.length - (position.column - 1), text); const slicedTokens = newTokens?.sliceAndInflate(position.column - 1, newLine.length, 0); if (!slicedTokens) { return text; diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts index 909e62f727c..0fe7ec7995c 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts @@ -127,7 +127,7 @@ suite('Suggest Widget Model', () => { const provider: CompletionItemProvider = { triggerCharacters: ['.'], async provideCompletionItems(model, pos) { - const word = model.getWordAtPosition(pos); + const word = model.tokenization.getWordAtPosition(pos); const range = word ? { startLineNumber: 1, startColumn: word.startColumn, endLineNumber: 1, endColumn: word.endColumn } : Range.fromPositions(pos); diff --git a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts index 81d40b01d43..a8d87224496 100644 --- a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts @@ -14,7 +14,7 @@ import { CompleteEnterAction, IndentAction } from 'vs/editor/common/languages/la import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IndentConsts } from 'vs/editor/common/languages/supports/indentRules'; import * as indentUtils from 'vs/editor/contrib/indentation/browser/indentUtils'; -import { getGoodIndentForLine, getIndentMetadata, IIndentConverter } from 'vs/editor/common/languages/autoIndent'; +import { getGoodIndentForLine, getIndentMetadata, IIndentConverter, IVirtualModel } from 'vs/editor/common/languages/autoIndent'; import { getEnterAction } from 'vs/editor/common/languages/enterAction'; export class MoveLinesCommand implements ICommand { @@ -63,15 +63,17 @@ export class MoveLinesCommand implements ICommand { const { tabSize, indentSize, insertSpaces } = model.getOptions(); let indentConverter = this.buildIndentConverter(tabSize, indentSize, insertSpaces); - let virtualModel = { - getLineTokens: (lineNumber: number) => { - return model.getLineTokens(lineNumber); - }, - getLanguageId: () => { - return model.getLanguageId(); - }, - getLanguageIdAtPosition: (lineNumber: number, column: number) => { - return model.getLanguageIdAtPosition(lineNumber, column); + let virtualModel: IVirtualModel = { + tokenization: { + getLineTokens: (lineNumber: number) => { + return model.tokenization.getLineTokens(lineNumber); + }, + getLanguageId: () => { + return model.getLanguageId(); + }, + getLanguageIdAtPosition: (lineNumber: number, column: number) => { + return model.getLanguageIdAtPosition(lineNumber, column); + }, }, getLineContent: null as unknown as (lineNumber: number) => string, }; @@ -361,7 +363,7 @@ export class MoveLinesCommand implements ICommand { return false; } // if it's not easy to tokenize, we stop auto indent. - if (!model.isCheapToTokenize(selection.startLineNumber)) { + if (!model.tokenization.isCheapToTokenize(selection.startLineNumber)) { return false; } let languageAtSelectionStart = model.getLanguageIdAtPosition(selection.startLineNumber, 1); diff --git a/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts b/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts index 93d7ee89440..f6390a36c4b 100644 --- a/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts +++ b/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts @@ -74,7 +74,7 @@ suite('linked editing', () => { disposables.add(languageFeaturesService.linkedEditingRangeProvider.register(mockFileSelector, { provideLinkedEditingRanges(model: ITextModel, pos: IPosition) { - const wordAtPos = model.getWordAtPosition(pos); + const wordAtPos = model.tokenization.getWordAtPosition(pos); if (wordAtPos) { const matches = model.findMatches(wordAtPos.word, false, false, true, USUAL_WORD_SEPARATORS, false); return { ranges: matches.map(m => m.range), wordPattern: initialState.responseWordPattern }; diff --git a/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index 13291e31fb6..7c554368fdf 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -74,7 +74,7 @@ class RenameSkeleton { return res; } - const word = this.model.getWordAtPosition(this.position); + const word = this.model.tokenization.getWordAtPosition(this.position); if (!word) { return { range: Range.fromPositions(this.position), diff --git a/src/vs/editor/contrib/smartSelect/browser/wordSelections.ts b/src/vs/editor/contrib/smartSelect/browser/wordSelections.ts index c98ad6a72f2..4ea0fdeda1a 100644 --- a/src/vs/editor/contrib/smartSelect/browser/wordSelections.ts +++ b/src/vs/editor/contrib/smartSelect/browser/wordSelections.ts @@ -26,7 +26,7 @@ export class WordSelectionRangeProvider implements SelectionRangeProvider { } private _addInWordRanges(bucket: SelectionRange[], model: ITextModel, pos: Position): void { - const obj = model.getWordAtPosition(pos); + const obj = model.tokenization.getWordAtPosition(pos); if (!obj) { return; } @@ -70,7 +70,7 @@ export class WordSelectionRangeProvider implements SelectionRangeProvider { } private _addWordRanges(bucket: SelectionRange[], model: ITextModel, pos: Position): void { - const word = model.getWordAtPosition(pos); + const word = model.tokenization.getWordAtPosition(pos); if (word) { bucket.push({ range: new Range(pos.lineNumber, word.startColumn, pos.lineNumber, word.endColumn) }); } diff --git a/src/vs/editor/contrib/snippet/browser/snippetController2.ts b/src/vs/editor/contrib/snippet/browser/snippetController2.ts index b1b4a6a1dd5..8beef71e950 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetController2.ts @@ -146,7 +146,7 @@ export class SnippetController2 implements IEditorContribution { return undefined; } - const info = model.getWordUntilPosition(position); + const info = model.tokenization.getWordUntilPosition(position); const isAnyOfOptions = Boolean(activeChoice.options.find(o => o.value === info.word)); const suggestions: CompletionItem[] = []; for (let i = 0; i < activeChoice.options.length; i++) { diff --git a/src/vs/editor/contrib/snippet/browser/snippetVariables.ts b/src/vs/editor/contrib/snippet/browser/snippetVariables.ts index 43825d087f9..50989118704 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetVariables.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetVariables.ts @@ -131,7 +131,7 @@ export class SelectionBasedVariableResolver implements VariableResolver { return this._model.getLineContent(this._selection.positionLineNumber); } else if (name === 'TM_CURRENT_WORD') { - const info = this._model.getWordAtPosition({ + const info = this._model.tokenization.getWordAtPosition({ lineNumber: this._selection.positionLineNumber, column: this._selection.positionColumn }); diff --git a/src/vs/editor/contrib/suggest/browser/suggest.ts b/src/vs/editor/contrib/suggest/browser/suggest.ts index 4a422eaef7d..3fb9cc5a1b5 100644 --- a/src/vs/editor/contrib/suggest/browser/suggest.ts +++ b/src/vs/editor/contrib/suggest/browser/suggest.ts @@ -207,7 +207,7 @@ export async function provideSuggestionItems( const sw = new StopWatch(true); position = position.clone(); - const word = model.getWordAtPosition(position); + const word = model.tokenization.getWordAtPosition(position); const defaultReplaceRange = word ? new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn) : Range.fromPositions(position); const defaultRange = { replace: defaultReplaceRange, insert: defaultReplaceRange.setEndPosition(position.lineNumber, position.column) }; diff --git a/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts b/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts index 40358156aac..02a980dc627 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts @@ -94,15 +94,15 @@ class SuggestInlineCompletions implements InlineCompletionsProvider this._removeOutstandingRequest(request), () => this._removeOutstandingRequest(request)); return request; } diff --git a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts index d2e3e3edd97..e2b4fb6c807 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts @@ -81,7 +81,7 @@ abstract class OccurenceAtPositionRequest implements IOccurenceAtPositionRequest protected abstract _compute(model: ITextModel, selection: Selection, wordSeparators: string, token: CancellationToken): Promise; private _getCurrentWordRange(model: ITextModel, selection: Selection): Range | null { - const word = model.getWordAtPosition(selection.getPosition()); + const word = model.tokenization.getWordAtPosition(selection.getPosition()); if (word) { return new Range(selection.startLineNumber, word.startColumn, selection.startLineNumber, word.endColumn); } @@ -145,7 +145,7 @@ class TextualOccurenceAtPositionRequest extends OccurenceAtPositionRequest { return []; } - const word = model.getWordAtPosition(selection.getPosition()); + const word = model.tokenization.getWordAtPosition(selection.getPosition()); if (!word || word.word.length > 1000) { return []; @@ -353,7 +353,7 @@ class WordHighlighter { let lineNumber = editorSelection.startLineNumber; let startColumn = editorSelection.startColumn; - return this.model.getWordAtPosition({ + return this.model.tokenization.getWordAtPosition({ lineNumber: lineNumber, column: startColumn }); diff --git a/src/vs/editor/standalone/browser/colorizer.ts b/src/vs/editor/standalone/browser/colorizer.ts index e30d28fbebb..7adcf0cea76 100644 --- a/src/vs/editor/standalone/browser/colorizer.ts +++ b/src/vs/editor/standalone/browser/colorizer.ts @@ -99,8 +99,8 @@ export class Colorizer { public static colorizeModelLine(model: ITextModel, lineNumber: number, tabSize: number = 4): string { const content = model.getLineContent(lineNumber); - model.forceTokenization(lineNumber); - const tokens = model.getLineTokens(lineNumber); + model.tokenization.forceTokenization(lineNumber); + const tokens = model.tokenization.getLineTokens(lineNumber); const inflatedTokens = tokens.inflate(); return this.colorizeLine(content, model.mightContainNonBasicASCII(), model.mightContainRTL(), inflatedTokens, tabSize); } diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 04e0f6fd2ca..eda68f3f1be 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -332,6 +332,7 @@ export function createMonacoEditorAPI(): typeof monaco.editor { WrappingIndent: standaloneEnums.WrappingIndent, InjectedTextCursorStops: standaloneEnums.InjectedTextCursorStops, PositionAffinity: standaloneEnums.PositionAffinity, + BackgroundTokenizationState: standaloneEnums.BackgroundTokenizationState, // classes ConfigurationChangedEvent: ConfigurationChangedEvent, diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index ca8a3522845..9243d63830b 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -442,7 +442,7 @@ export function registerHoverProvider(languageSelector: LanguageSelector, provid const languageFeaturesService = StandaloneServices.get(ILanguageFeaturesService); return languageFeaturesService.hoverProvider.register(languageSelector, { provideHover: (model: model.ITextModel, position: Position, token: CancellationToken): Promise => { - const word = model.getWordAtPosition(position); + const word = model.tokenization.getWordAtPosition(position); return Promise.resolve(provider.provideHover(model, position, token)).then((value): languages.Hover | undefined => { if (!value) { diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 2fad7ea8bea..56a7fc25f84 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -2751,7 +2751,7 @@ suite('Editor Controller', () => { withTestCodeEditor(model, {}, (editor2, cursor2) => { editor1.onDidChangeCursorPosition(() => { - model.tokenizeIfCheap(1); + model.tokenization.tokenizeIfCheap(1); }); model.applyEdits([{ range: new Range(1, 1, 1, 1), text: '-' }]); @@ -3680,7 +3680,7 @@ suite('Editor Controller', () => { assertCursor(viewModel, new Selection(1, 12, 1, 12)); viewModel.type('\n', 'keyboard'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertCursor(viewModel, new Selection(2, 2, 2, 2)); moveTo(editor, viewModel, 3, 13, false); @@ -3743,7 +3743,7 @@ suite('Editor Controller', () => { assertCursor(viewModel, new Selection(2, 14, 2, 14)); viewModel.type('\n', 'keyboard'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertCursor(viewModel, new Selection(3, 1, 3, 1)); moveTo(editor, viewModel, 5, 16, false); @@ -3771,7 +3771,7 @@ suite('Editor Controller', () => { assertCursor(viewModel, new Selection(2, 11, 2, 11)); viewModel.type('\n', 'keyboard'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertCursor(viewModel, new Selection(3, 3, 3, 3)); viewModel.type('console.log();', 'keyboard'); @@ -3856,7 +3856,7 @@ suite('Editor Controller', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(2, 5, 2, 5)); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); moveTo(editor, viewModel, 3, 13, false); assertCursor(viewModel, new Selection(3, 13, 3, 13)); @@ -3878,7 +3878,7 @@ suite('Editor Controller', () => { assertCursor(viewModel, new Selection(1, 12, 1, 12)); viewModel.type('\n', 'keyboard'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertCursor(viewModel, new Selection(2, 5, 2, 5)); moveTo(editor, viewModel, 3, 16, false); @@ -3903,7 +3903,7 @@ suite('Editor Controller', () => { assertCursor(viewModel, new Selection(1, 12, 1, 12)); viewModel.type('\n', 'keyboard'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertCursor(viewModel, new Selection(2, 2, 2, 2)); moveTo(editor, viewModel, 3, 16, false); @@ -4614,13 +4614,13 @@ suite('Editor Controller', () => { assertCursor(viewModel, new Selection(1, 9, 1, 9)); viewModel.type('\n', 'keyboard'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertCursor(viewModel, new Selection(2, 2, 2, 2)); moveTo(editor, viewModel, 1, 9, false); assertCursor(viewModel, new Selection(1, 9, 1, 9)); viewModel.type('\n', 'keyboard'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertCursor(viewModel, new Selection(2, 2, 2, 2)); }); }); @@ -4931,7 +4931,7 @@ suite('Editor Controller', () => { text: ['const markup = highlight'], languageId: autoClosingLanguageId }, (editor, model, viewModel) => { - model.forceTokenization(1); + model.tokenization.forceTokenization(1); assertType(editor, model, viewModel, 1, 25, '`', '``', `auto closes \` @ (1, 25)`); }); }); @@ -4944,7 +4944,7 @@ suite('Editor Controller', () => { {}, (editor, viewModel) => { const model = viewModel.model; - model.forceTokenization(1); + model.tokenization.forceTokenization(1); assertType(editor, model, viewModel, 1, 28, '`', '`', `does not auto close \` @ (1, 28)`); } ); @@ -4980,7 +4980,7 @@ suite('Editor Controller', () => { const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { @@ -5024,7 +5024,7 @@ suite('Editor Controller', () => { const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { @@ -5055,7 +5055,7 @@ suite('Editor Controller', () => { const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { @@ -5085,7 +5085,7 @@ suite('Editor Controller', () => { const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '\'', '\'\'', `auto closes @ (${lineNumber}, ${column})`); } else { @@ -5131,7 +5131,7 @@ suite('Editor Controller', () => { const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { @@ -5176,7 +5176,7 @@ suite('Editor Controller', () => { const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); assertType(editor, model, viewModel, lineNumber, column, '"', '""', `auto closes @ (${lineNumber}, ${column})`); @@ -5310,7 +5310,7 @@ suite('Editor Controller', () => { const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '\'', '\'\'', `auto closes @ (${lineNumber}, ${column})`); } else if (autoCloseColumns[column] === AutoClosingColumnType.Special2) { @@ -5408,15 +5408,15 @@ suite('Editor Controller', () => { ], languageId: languageId }, (editor, model, viewModel) => { - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertType(editor, model, viewModel, 1, 4, '"', '"', `does not double quote when ending with open`); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertType(editor, model, viewModel, 2, 4, '"', '"', `does not double quote when ending with open`); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertType(editor, model, viewModel, 3, 4, '"', '"', `does not double quote when ending with open`); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertType(editor, model, viewModel, 4, 2, '"', '"', `does not double quote when ending with open`); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertType(editor, model, viewModel, 4, 3, '"', '"', `does not double quote when ending with open`); }); }); @@ -5447,50 +5447,50 @@ suite('Editor Controller', () => { } // First gif - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste1 = teste\' ok'); assert.strictEqual(model.getLineContent(1), 'teste1 = teste\' ok'); viewModel.setSelections('test', [new Selection(1, 1000, 1, 1000)]); typeCharacters(viewModel, '\n'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste2 = teste \'ok'); assert.strictEqual(model.getLineContent(2), 'teste2 = teste \'ok\''); viewModel.setSelections('test', [new Selection(2, 1000, 2, 1000)]); typeCharacters(viewModel, '\n'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste3 = teste" ok'); assert.strictEqual(model.getLineContent(3), 'teste3 = teste" ok'); viewModel.setSelections('test', [new Selection(3, 1000, 3, 1000)]); typeCharacters(viewModel, '\n'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste4 = teste "ok'); assert.strictEqual(model.getLineContent(4), 'teste4 = teste "ok"'); // Second gif viewModel.setSelections('test', [new Selection(4, 1000, 4, 1000)]); typeCharacters(viewModel, '\n'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste \''); assert.strictEqual(model.getLineContent(5), 'teste \'\''); viewModel.setSelections('test', [new Selection(5, 1000, 5, 1000)]); typeCharacters(viewModel, '\n'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste "'); assert.strictEqual(model.getLineContent(6), 'teste ""'); viewModel.setSelections('test', [new Selection(6, 1000, 6, 1000)]); typeCharacters(viewModel, '\n'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste\''); assert.strictEqual(model.getLineContent(7), 'teste\''); viewModel.setSelections('test', [new Selection(7, 1000, 7, 1000)]); typeCharacters(viewModel, '\n'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste"'); assert.strictEqual(model.getLineContent(8), 'teste"'); }); diff --git a/src/vs/editor/test/browser/testCommand.ts b/src/vs/editor/test/browser/testCommand.ts index 22d6a6fc9f2..d8349dd08d8 100644 --- a/src/vs/editor/test/browser/testCommand.ts +++ b/src/vs/editor/test/browser/testCommand.ts @@ -34,7 +34,7 @@ export function testCommand( const viewModel = editor.getViewModel()!; if (forceTokenization) { - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); } viewModel.setSelections('tests', [selection]); diff --git a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts index 88c17e7823d..2fe1d779919 100644 --- a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts +++ b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts @@ -352,7 +352,7 @@ suite('SplitLinesCollection', () => { languageRegistration = languages.TokenizationRegistry.register(LANGUAGE_ID, tokenizationSupport); model = createTextModel(_text.join('\n'), LANGUAGE_ID); // force tokenization - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); }); teardown(() => { @@ -988,8 +988,10 @@ function createLineBreakData(breakingLengths: number[], breakingOffsetsVisibleCo function createModel(text: string): ISimpleModel { return { - getLineTokens: (lineNumber: number) => { - return null!; + tokenization: { + getLineTokens: (lineNumber: number) => { + return null!; + }, }, getLineContent: (lineNumber: number) => { return text; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts index 6871809fb7b..77950e40634 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts @@ -40,7 +40,7 @@ suite('Bracket Pair Colorizer - Tokenizer', () => { })); const model = disposableStore.add(instantiateTextModel(instantiationService, document.getText(), mode1)); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); const brackets = new LanguageAgnosticBracketTokens(denseKeyProvider, l => languageConfigurationService.getLanguageConfiguration(l)); diff --git a/src/vs/editor/test/common/model/model.line.test.ts b/src/vs/editor/test/common/model/model.line.test.ts index 541f39e0d39..7424e5f573e 100644 --- a/src/vs/editor/test/common/model/model.line.test.ts +++ b/src/vs/editor/test/common/model/model.line.test.ts @@ -125,7 +125,7 @@ suite('ModelLinesTokens', () => { for (let lineIndex = 0; lineIndex < expected.length; lineIndex++) { const actualLine = model.getLineContent(lineIndex + 1); - const actualTokens = model.getLineTokens(lineIndex + 1); + const actualTokens = model.tokenization.getLineTokens(lineIndex + 1); assert.strictEqual(actualLine, expected[lineIndex].text); assertLineTokens(actualTokens, expected[lineIndex].tokens); } @@ -462,7 +462,7 @@ suite('ModelLinesTokens', () => { text: 'a' }]); - const actualTokens = model.getLineTokens(1); + const actualTokens = model.tokenization.getLineTokens(1); assertLineTokens(actualTokens, [new TestToken(0, 1)]); model.dispose(); diff --git a/src/vs/editor/test/common/model/model.modes.test.ts b/src/vs/editor/test/common/model/model.modes.test.ts index 8f3bf8433ef..c6287828d6d 100644 --- a/src/vs/editor/test/common/model/model.modes.test.ts +++ b/src/vs/editor/test/common/model/model.modes.test.ts @@ -56,98 +56,98 @@ suite('Editor Model - Model Modes 1', () => { }); test('model calls syntax highlighter 1', () => { - thisModel.forceTokenization(1); + thisModel.tokenization.forceTokenization(1); checkAndClear(['1']); }); test('model calls syntax highlighter 2', () => { - thisModel.forceTokenization(2); + thisModel.tokenization.forceTokenization(2); checkAndClear(['1', '2']); - thisModel.forceTokenization(2); + thisModel.tokenization.forceTokenization(2); checkAndClear([]); }); test('model caches states', () => { - thisModel.forceTokenization(1); + thisModel.tokenization.forceTokenization(1); checkAndClear(['1']); - thisModel.forceTokenization(2); + thisModel.tokenization.forceTokenization(2); checkAndClear(['2']); - thisModel.forceTokenization(3); + thisModel.tokenization.forceTokenization(3); checkAndClear(['3']); - thisModel.forceTokenization(4); + thisModel.tokenization.forceTokenization(4); checkAndClear(['4']); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['5']); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear([]); }); test('model invalidates states for one line insert', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['1', '2', '3', '4', '5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '-')]); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['-']); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear([]); }); test('model invalidates states for many lines insert', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['1', '2', '3', '4', '5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '0\n-\n+')]); assert.strictEqual(thisModel.getLineCount(), 7); - thisModel.forceTokenization(7); + thisModel.tokenization.forceTokenization(7); checkAndClear(['0', '-', '+']); - thisModel.forceTokenization(7); + thisModel.tokenization.forceTokenization(7); checkAndClear([]); }); test('model invalidates states for one new line', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['1', '2', '3', '4', '5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 2), '\n')]); thisModel.applyEdits([EditOperation.insert(new Position(2, 1), 'a')]); - thisModel.forceTokenization(6); + thisModel.tokenization.forceTokenization(6); checkAndClear(['1', 'a']); }); test('model invalidates states for one line delete', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['1', '2', '3', '4', '5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 2), '-')]); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['1']); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['-']); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear([]); }); test('model invalidates states for many lines delete', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['1', '2', '3', '4', '5']); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 3, 1))]); - thisModel.forceTokenization(3); + thisModel.tokenization.forceTokenization(3); checkAndClear(['3']); - thisModel.forceTokenization(3); + thisModel.tokenization.forceTokenization(3); checkAndClear([]); }); }); @@ -208,55 +208,55 @@ suite('Editor Model - Model Modes 2', () => { }); test('getTokensForInvalidLines one text insert', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1', 'Line2', 'Line3', 'Line4', 'Line5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 6), '-')]); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1-', 'Line2']); }); test('getTokensForInvalidLines two text insert', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1', 'Line2', 'Line3', 'Line4', 'Line5']); thisModel.applyEdits([ EditOperation.insert(new Position(1, 6), '-'), EditOperation.insert(new Position(3, 6), '-') ]); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1-', 'Line2', 'Line3-', 'Line4']); }); test('getTokensForInvalidLines one multi-line text insert, one small text insert', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1', 'Line2', 'Line3', 'Line4', 'Line5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 6), '\nNew line\nAnother new line')]); thisModel.applyEdits([EditOperation.insert(new Position(5, 6), '-')]); - thisModel.forceTokenization(7); + thisModel.tokenization.forceTokenization(7); checkAndClear(['Line1', 'New line', 'Another new line', 'Line2', 'Line3-', 'Line4']); }); test('getTokensForInvalidLines one delete text', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1', 'Line2', 'Line3', 'Line4', 'Line5']); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 5))]); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['1', 'Line2']); }); test('getTokensForInvalidLines one line delete text', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1', 'Line2', 'Line3', 'Line4', 'Line5']); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 2, 1))]); - thisModel.forceTokenization(4); + thisModel.tokenization.forceTokenization(4); checkAndClear(['Line2']); }); test('getTokensForInvalidLines multiple lines delete text', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1', 'Line2', 'Line3', 'Line4', 'Line5']); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 3, 3))]); - thisModel.forceTokenization(3); + thisModel.tokenization.forceTokenization(3); checkAndClear(['ne3', 'Line4']); }); }); diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index 1b1f31881be..ede228ffd7c 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -442,17 +442,17 @@ suite('Editor Model - Words', () => { const thisModel = createTextModel(text.join('\n')); disposables.push(thisModel); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: 'text', startColumn: 6, endColumn: 10 }); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 19)), { word: 'some', startColumn: 15, endColumn: 19 }); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 20)), null); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 21)), { word: 'words', startColumn: 21, endColumn: 26 }); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 26)), { word: 'words', startColumn: 21, endColumn: 26 }); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 27)), null); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 28)), null); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 1)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 2)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 4)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 5)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 6)), { word: 'text', startColumn: 6, endColumn: 10 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 19)), { word: 'some', startColumn: 15, endColumn: 19 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 20)), null); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 21)), { word: 'words', startColumn: 21, endColumn: 26 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 26)), { word: 'words', startColumn: 21, endColumn: 26 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 27)), null); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 28)), null); }); test('getWordAtPosition at embedded language boundaries', () => { @@ -463,13 +463,13 @@ suite('Editor Model - Words', () => { const model = disposables.add(instantiateTextModel(instantiationService, 'abab', outerMode.languageId)); - assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 1)), { word: 'ab', startColumn: 1, endColumn: 3 }); - assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 2)), { word: 'ab', startColumn: 1, endColumn: 3 }); - assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 3)), { word: 'ab', startColumn: 1, endColumn: 3 }); - assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 4)), { word: 'xx', startColumn: 4, endColumn: 6 }); - assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 5)), { word: 'xx', startColumn: 4, endColumn: 6 }); - assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 6)), { word: 'xx', startColumn: 4, endColumn: 6 }); - assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 7)), { word: 'ab', startColumn: 7, endColumn: 9 }); + assert.deepStrictEqual(model.tokenization.getWordAtPosition(new Position(1, 1)), { word: 'ab', startColumn: 1, endColumn: 3 }); + assert.deepStrictEqual(model.tokenization.getWordAtPosition(new Position(1, 2)), { word: 'ab', startColumn: 1, endColumn: 3 }); + assert.deepStrictEqual(model.tokenization.getWordAtPosition(new Position(1, 3)), { word: 'ab', startColumn: 1, endColumn: 3 }); + assert.deepStrictEqual(model.tokenization.getWordAtPosition(new Position(1, 4)), { word: 'xx', startColumn: 4, endColumn: 6 }); + assert.deepStrictEqual(model.tokenization.getWordAtPosition(new Position(1, 5)), { word: 'xx', startColumn: 4, endColumn: 6 }); + assert.deepStrictEqual(model.tokenization.getWordAtPosition(new Position(1, 6)), { word: 'xx', startColumn: 4, endColumn: 6 }); + assert.deepStrictEqual(model.tokenization.getWordAtPosition(new Position(1, 7)), { word: 'ab', startColumn: 7, endColumn: 9 }); disposables.dispose(); }); @@ -488,14 +488,14 @@ suite('Editor Model - Words', () => { const thisModel = disposables.add(instantiateTextModel(instantiationService, '.🐷-a-b', MODE_ID)); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: '.', startColumn: 1, endColumn: 2 }); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: '.', startColumn: 1, endColumn: 2 }); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 3)), null); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 7)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 8)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 1)), { word: '.', startColumn: 1, endColumn: 2 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 2)), { word: '.', startColumn: 1, endColumn: 2 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 3)), null); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 4)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 5)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 6)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 7)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.tokenization.getWordAtPosition(new Position(1, 8)), { word: '-a-b', startColumn: 4, endColumn: 8 }); disposables.dispose(); }); diff --git a/src/vs/editor/test/common/model/textModelWithTokens.test.ts b/src/vs/editor/test/common/model/textModelWithTokens.test.ts index 59d3450eedb..75e0e8f4c89 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -438,9 +438,9 @@ suite('TextModelWithTokens', () => { mode1 )); - model.forceTokenization(1); - model.forceTokenization(2); - model.forceTokenization(3); + model.tokenization.forceTokenization(1); + model.tokenization.forceTokenization(2); + model.tokenization.forceTokenization(3); assert.deepStrictEqual(model.bracketPairs.matchBracket(new Position(2, 14)), [new Range(2, 13, 2, 14), new Range(2, 18, 2, 19)]); @@ -517,9 +517,9 @@ suite('TextModelWithTokens', () => { mode )); - model.forceTokenization(1); - model.forceTokenization(2); - model.forceTokenization(3); + model.tokenization.forceTokenization(1); + model.tokenization.forceTokenization(2); + model.tokenization.forceTokenization(3); assert.deepStrictEqual(model.bracketPairs.matchBracket(new Position(2, 23)), null); assert.deepStrictEqual(model.bracketPairs.matchBracket(new Position(2, 20)), null); @@ -534,9 +534,9 @@ suite('TextModelWithTokens regression tests', () => { test('microsoft/monaco-editor#122: Unhandled Exception: TypeError: Unable to get property \'replace\' of undefined or null reference', () => { function assertViewLineTokens(model: TextModel, lineNumber: number, forceTokenization: boolean, expected: TestLineToken[]): void { if (forceTokenization) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); } - let _actual = model.getLineTokens(lineNumber).inflate(); + let _actual = model.tokenization.getLineTokens(lineNumber).inflate(); interface ISimpleViewToken { endIndex: number; foreground: number; @@ -688,7 +688,7 @@ suite('TextModelWithTokens regression tests', () => { const model = disposables.add(instantiateTextModel(instantiationService, 'A model with one line', outerMode)); - model.forceTokenization(1); + model.tokenization.forceTokenization(1); assert.strictEqual(model.getLanguageIdAtPosition(1, 1), innerMode); disposables.dispose(); diff --git a/src/vs/editor/test/common/model/tokensStore.test.ts b/src/vs/editor/test/common/model/tokensStore.test.ts index ba69c425adf..680503c31c2 100644 --- a/src/vs/editor/test/common/model/tokensStore.test.ts +++ b/src/vs/editor/test/common/model/tokensStore.test.ts @@ -74,7 +74,7 @@ suite('TokensStore', () => { function extractState(model: TextModel): string[] { let result: string[] = []; for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) { - const lineTokens = model.getLineTokens(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); const lineContent = model.getLineContent(lineNumber); let lineText = ''; @@ -101,7 +101,7 @@ suite('TokensStore', () => { function testTokensAdjustment(rawInitialState: string[], edits: ISingleEditOperation[], rawFinalState: string[]) { const initialState = parseTokensState(rawInitialState); const model = createTextModel(initialState.text); - model.setSemanticTokens([initialState.tokens], true); + model.tokenization.setSemanticTokens([initialState.tokens], true); model.applyEdits(edits); @@ -174,7 +174,7 @@ suite('TokensStore', () => { test('issue #91936: Semantic token color highlighting fails on line with selected text', () => { const model = createTextModel(' else if ($s = 08) then \'\\b\''); - model.setSemanticTokens([ + model.tokenization.setSemanticTokens([ SparseMultilineTokens.create(1, new Uint32Array([ 0, 20, 24, 0b0111100000000010000, 0, 25, 27, 0b0111100000000010000, @@ -187,7 +187,7 @@ suite('TokensStore', () => { 0, 43, 47, 0b0101100000000010000, ])) ], true); - const lineTokens = model.getLineTokens(1); + const lineTokens = model.tokenization.getLineTokens(1); let decodedTokens: number[] = []; for (let i = 0, len = lineTokens.getCount(); i < len; i++) { decodedTokens.push(lineTokens.getEndOffset(i), lineTokens.getMetadata(i)); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 4ce9d88a0d8..ad74972c927 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1396,6 +1396,32 @@ declare namespace monaco.editor { readonly endColumn: number; } + export interface ITokenizationTextModelPart { + /** + * Get the word under or besides `position`. + * @param position The position to look for a word. + * @return The word under or besides `position`. Might be null. + */ + getWordAtPosition(position: IPosition): IWordAtPosition | null; + /** + * Get the word under or besides `position` trimmed to `position`.column + * @param position The position to look for a word. + * @return The word under or besides `position`. Will never be null. + */ + getWordUntilPosition(position: IPosition): IWordAtPosition; + getLanguageId(): string; + getLanguageIdAtPosition(lineNumber: number, column: number): string; + setLanguageId(languageId: string): void; + readonly backgroundTokenizationState: BackgroundTokenizationState; + readonly onBackgroundTokenizationStateChanged: IEvent; + } + + export enum BackgroundTokenizationState { + Uninitialized = 0, + InProgress = 1, + Completed = 2 + } + /** * Vertical Lane in the overview ruler of the editor. */ @@ -1911,18 +1937,6 @@ declare namespace monaco.editor { * Get the language associated with this model. */ getLanguageId(): string; - /** - * Get the word under or besides `position`. - * @param position The position to look for a word. - * @return The word under or besides `position`. Might be null. - */ - getWordAtPosition(position: IPosition): IWordAtPosition | null; - /** - * Get the word under or besides `position` trimmed to `position`.column - * @param position The position to look for a word. - * @return The word under or besides `position`. Will never be null. - */ - getWordUntilPosition(position: IPosition): IWordAtPosition; /** * Perform a minimum amount of operations, in order to transform the decorations * identified by `oldDecorations` to the decorations described by `newDecorations` @@ -2082,6 +2096,7 @@ declare namespace monaco.editor { * Returns if this model is attached to an editor or not. */ isAttachedToEditor(): boolean; + readonly tokenization: ITokenizationTextModelPart; } export enum PositionAffinity { diff --git a/src/vs/workbench/api/browser/mainThreadLanguages.ts b/src/vs/workbench/api/browser/mainThreadLanguages.ts index 71c8c4ab1c2..e8316e42dfb 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguages.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguages.ts @@ -68,8 +68,8 @@ export class MainThreadLanguages implements MainThreadLanguagesShape { if (!model) { return undefined; } - model.tokenizeIfCheap(position.lineNumber); - const tokens = model.getLineTokens(position.lineNumber); + model.tokenization.tokenizeIfCheap(position.lineNumber); + const tokens = model.tokenization.getLineTokens(position.lineNumber); const idx = tokens.findTokenIndexAtOffset(position.column - 1); return { type: tokens.getStandardTokenType(idx), diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index c796f5e0ce4..ca1856c6312 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -227,14 +227,14 @@ export class BulkEditDataSource implements IAsyncDataSource= 0; idx--) { prefixLen = range.startColumn - startTokens.getStartOffset(idx); } //suffix-math - let endTokens = textModel.getLineTokens(range.endLineNumber); + let endTokens = textModel.tokenization.getLineTokens(range.endLineNumber); let suffixLen = 0; for (let idx = endTokens.findTokenIndexAtOffset(range.endColumn); suffixLen < 50 && idx < endTokens.getCount(); idx++) { suffixLen += endTokens.getEndOffset(idx) - endTokens.getStartOffset(idx); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index bcb3748c6a1..04dd1f1b1fe 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -313,7 +313,7 @@ class ShowDebugHoverAction extends EditorAction { if (!position || !editor.hasModel()) { return; } - const word = editor.getModel().getWordAtPosition(position); + const word = editor.getModel().tokenization.getWordAtPosition(position); if (!word) { return; } diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 81133f8fade..ee581fd735b 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -179,8 +179,8 @@ function getWordToLineNumbersMap(model: ITextModel | null): Map => { const getWordRangeAtPosition = (model: ITextModel, position: Position): Range | null => { - const wordAtPosition = model.getWordAtPosition(position); + const wordAtPosition = model.tokenization.getWordAtPosition(position); return wordAtPosition ? new Range(position.lineNumber, wordAtPosition.startColumn, position.lineNumber, wordAtPosition.endColumn) : null; }; diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts index 46388b581b4..43c5852753e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts @@ -65,7 +65,7 @@ class EditorTextRenderer { let result = ''; for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { - const lineTokens = model.getLineTokens(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); const lineContent = lineTokens.getLineContent(); const startOffset = (lineNumber === startLineNumber ? startColumn - 1 : 0); const endOffset = (lineNumber === endLineNumber ? endColumn - 1 : lineContent.length); diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 90411d0caec..eddb57e8663 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -1160,7 +1160,7 @@ export class SearchView extends ViewPane { } if (range.isEmpty() && this.searchConfig.seedWithNearestWord && allowUnselectedWord) { - const wordAtPosition = editor.getModel().getWordAtPosition(range.getStartPosition()); + const wordAtPosition = editor.getModel().tokenization.getWordAtPosition(range.getStartPosition()); if (wordAtPosition) { return wordAtPosition.word; } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index 28e8b4fd1e5..76fa794a03a 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -127,7 +127,7 @@ export const openNewSearchEditor = selected = (selection && activeModel?.getModel()?.getValueInRange(selection)) ?? ''; if (selection?.isEmpty() && configurationService.getValue('search').seedWithNearestWord) { - const wordAtPosition = activeModel.getModel()?.getWordAtPosition(selection.getStartPosition()); + const wordAtPosition = activeModel.getModel()?.tokenization.getWordAtPosition(selection.getStartPosition()); if (wordAtPosition) { selected = wordAtPosition.word; } diff --git a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts index 782cb1493fd..3a0088a1f48 100644 --- a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts @@ -112,7 +112,7 @@ class InsertSnippetAction extends EditorAction { } languageId = langId; } else { - editor.getModel().tokenizeIfCheap(lineNumber); + editor.getModel().tokenization.tokenizeIfCheap(lineNumber); languageId = editor.getModel().getLanguageIdAtPosition(lineNumber, column); // validate the `languageId` to ensure this is a user diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index aa6e96d9afe..b2b27cc1db9 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -73,7 +73,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { const snippets = new Set(await this._snippets.getSnippets(languageId)); const lineContentLow = model.getLineContent(position.lineNumber).toLowerCase(); - const wordUntil = model.getWordUntilPosition(position).word.toLowerCase(); + const wordUntil = model.tokenization.getWordUntilPosition(position).word.toLowerCase(); const suggestions: SnippetCompletion[] = []; const columnOffset = position.column - 1; @@ -182,7 +182,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { // validate the `languageId` to ensure this is a user // facing language with a name and the chance to have // snippets, else fall back to the outer language - model.tokenizeIfCheap(position.lineNumber); + model.tokenization.tokenizeIfCheap(position.lineNumber); let languageId = model.getLanguageIdAtPosition(position.lineNumber, position.column); if (!this._languageService.getLanguageName(languageId)) { languageId = model.getLanguageId(); diff --git a/src/vs/workbench/contrib/snippets/browser/surroundWithSnippet.ts b/src/vs/workbench/contrib/snippets/browser/surroundWithSnippet.ts index 96c1ecfc5a7..80d25b05968 100644 --- a/src/vs/workbench/contrib/snippets/browser/surroundWithSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/surroundWithSnippet.ts @@ -38,7 +38,7 @@ registerAction2(class SurroundWithAction extends EditorAction2 { } const { lineNumber, column } = editor.getPosition(); - editor.getModel().tokenizeIfCheap(lineNumber); + editor.getModel().tokenization.tokenizeIfCheap(lineNumber); const languageId = editor.getModel().getLanguageIdAtPosition(lineNumber, column); const allSnippets = await snippetService.getSnippets(languageId, { includeNoPrefixSnippets: true, includeDisabledSnippets: true }); diff --git a/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts b/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts index 504da91fa71..0ed0cc9470b 100644 --- a/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts +++ b/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts @@ -91,7 +91,7 @@ export class TabCompletionController implements IEditorContribution { // lots of dance for getting the const selection = this._editor.getSelection(); const model = this._editor.getModel(); - model.tokenizeIfCheap(selection.positionLineNumber); + model.tokenization.tokenizeIfCheap(selection.positionLineNumber); const id = model.getLanguageIdAtPosition(selection.positionLineNumber, selection.positionColumn); const snippets = this._snippetService.getSnippetsSync(id); diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index df0250c0d61..000a74b77ca 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -903,7 +903,7 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements // Force tokenization now - otherwise it may be slightly delayed, causing a flash of white text const tokenizeTo = Math.min(startLine + 60, this._model.getLineCount()); - this._model.forceTokenization(tokenizeTo); + this._model.tokenization.forceTokenization(tokenizeTo); return { matches, settingsGroups }; } diff --git a/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts b/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts index de5b056af74..bbf4fddc3e0 100644 --- a/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts @@ -120,7 +120,7 @@ class ModelWorkerTextMateTokenizer extends Disposable { } } - this._model.setTokens(tokens); + this._model.tokenization.setTokens(tokens); } } -- cgit v1.2.3 From 9fe18d2e6b39e321fa9c0d780a30734b1366546f Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 1 Apr 2022 23:12:19 +0530 Subject: fix layering --- .../sharedProcess/sharedProcessMain.ts | 5 +-- .../common/extensionsScannerService.ts | 22 ++++++------- .../extensionManagement/node/extensionsScanner.ts | 4 +-- .../node/extensionsScannerService.ts | 35 +++++++++++++++++++++ .../test/node/extensionsScannerService.test.ts | 35 +++++++++++---------- src/vs/server/node/remoteAgentEnvironmentImpl.ts | 4 +-- src/vs/server/node/serverServices.ts | 7 +++-- .../electron-sandbox/extensionsScannerService.ts | 36 ++++++++++++++++++++++ .../electron-sandbox/cachedExtensionScanner.ts | 4 +-- src/vs/workbench/workbench.common.main.ts | 1 - src/vs/workbench/workbench.sandbox.main.ts | 1 + 11 files changed, 112 insertions(+), 42 deletions(-) create mode 100644 src/vs/platform/extensionManagement/node/extensionsScannerService.ts create mode 100644 src/vs/workbench/services/extensionManagement/electron-sandbox/extensionsScannerService.ts (limited to 'src/vs') diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index c378b1c8bde..3687285e230 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -98,7 +98,8 @@ import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataPr import { DiskFileSystemProviderClient, LOCAL_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/files/common/diskFileSystemProviderClient'; import { InspectProfilingService as V8InspectProfilingService } from 'vs/platform/profiling/node/profilingService'; import { IV8InspectProfilingService } from 'vs/platform/profiling/common/profiling'; -import { NativeExtensionsScannerService, INativeExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService'; class SharedProcessMain extends Disposable { @@ -294,7 +295,7 @@ class SharedProcessMain extends Disposable { services.set(ICustomEndpointTelemetryService, customEndpointTelemetryService); // Extension Management - services.set(INativeExtensionsScannerService, new SyncDescriptor(NativeExtensionsScannerService)); + services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); // Extension Gallery diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index e6cc2f4aa42..3db83fd9409 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -18,13 +18,12 @@ import Severity from 'vs/base/common/severity'; import { isArray, isObject, isString } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -107,8 +106,8 @@ export type ScanOptions = { readonly checkControlFile?: boolean; }; -export const INativeExtensionsScannerService = createDecorator('INativeExtensionsScannerService'); -export interface INativeExtensionsScannerService { +export const IExtensionsScannerService = createDecorator('IExtensionsScannerService'); +export interface IExtensionsScannerService { readonly _serviceBrand: undefined; readonly systemExtensionsLocation: URI; @@ -126,22 +125,21 @@ export interface INativeExtensionsScannerService { updateMetadata(extensionLocation: URI, metadata: Partial): Promise; } -export class NativeExtensionsScannerService extends Disposable implements INativeExtensionsScannerService { +export abstract class AbstractExtensionsScannerService extends Disposable implements IExtensionsScannerService { readonly _serviceBrand: undefined; - readonly systemExtensionsLocation: URI; - readonly userExtensionsLocation: URI; + abstract readonly systemExtensionsLocation: URI; + abstract readonly userExtensionsLocation: URI; + protected abstract readonly extensionsControlLocation: URI; constructor( @IFileService private readonly fileService: IFileService, @ILogService private readonly logService: ILogService, - @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, + @INativeEnvironmentService private readonly environmentService: IEnvironmentService, @IProductService private readonly productService: IProductService, ) { super(); - this.systemExtensionsLocation = URI.file(environmentService.builtinExtensionsPath); - this.userExtensionsLocation = URI.file(environmentService.extensionsPath); } private _targetPlatformPromise: Promise | undefined; @@ -310,7 +308,7 @@ export class NativeExtensionsScannerService extends Disposable implements INativ private async getBuiltInExtensionControl(): Promise { try { - const content = await this.fileService.readFile(joinPath(this.environmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json')); + const content = await this.fileService.readFile(this.extensionsControlLocation); return JSON.parse(content.value.toString()); } catch (error) { return {}; @@ -658,5 +656,3 @@ export function toExtensionDescription(extension: IScannedExtension, isUnderDeve ...extension.manifest, }; } - -registerSingleton(INativeExtensionsScannerService, NativeExtensionsScannerService); diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts index c47171faf08..b725a35ad7b 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -20,7 +20,7 @@ import { extract, ExtractError } from 'vs/base/node/zip'; import { localize } from 'vs/nls'; import { ExtensionManagementError, ExtensionManagementErrorCode, Metadata, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { INativeExtensionsScannerService, IScannedExtension, ScanOptions } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { IExtensionsScannerService, IScannedExtension, ScanOptions } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; @@ -33,7 +33,7 @@ export class ExtensionsScanner extends Disposable { constructor( private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise, @IFileService private readonly fileService: IFileService, - @INativeExtensionsScannerService private readonly extensionsScannerService: INativeExtensionsScannerService, + @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, @ILogService private readonly logService: ILogService, ) { super(); diff --git a/src/vs/platform/extensionManagement/node/extensionsScannerService.ts b/src/vs/platform/extensionManagement/node/extensionsScannerService.ts new file mode 100644 index 00000000000..20ff1608038 --- /dev/null +++ b/src/vs/platform/extensionManagement/node/extensionsScannerService.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { joinPath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { AbstractExtensionsScannerService, IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; + +export class ExtensionsScannerService extends AbstractExtensionsScannerService implements IExtensionsScannerService { + + readonly systemExtensionsLocation: URI; + readonly userExtensionsLocation: URI; + protected readonly extensionsControlLocation: URI; + + constructor( + @IFileService fileService: IFileService, + @ILogService logService: ILogService, + @INativeEnvironmentService environmentService: INativeEnvironmentService, + @IProductService productService: IProductService, + ) { + super(fileService, logService, environmentService, productService); + this.systemExtensionsLocation = URI.file(environmentService.builtinExtensionsPath); + this.userExtensionsLocation = URI.file(environmentService.extensionsPath); + this.extensionsControlLocation = joinPath(environmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json'); + } + +} + +registerSingleton(IExtensionsScannerService, ExtensionsScannerService); diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index 60b80de354d..938adcbd388 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -9,7 +9,8 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { dirname, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { INativeExtensionsScannerService, IScannedExtensionManifest, NativeExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { IExtensionsScannerService, IScannedExtensionManifest } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -46,7 +47,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan system extension', async () => { const manifest: Partial = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }); const extensionLocation = await aSystemExtension(manifest); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanSystemExtensions({}); @@ -65,7 +66,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan user extension', async () => { const manifest: Partial = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }); const extensionLocation = await aUserExtension(manifest); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanUserExtensions({}); @@ -84,7 +85,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan existing extension', async () => { const manifest: Partial = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }); const extensionLocation = await aUserExtension(manifest); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanExistingExtension(extensionLocation, ExtensionType.User, {}); @@ -103,7 +104,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan single extension', async () => { const manifest: Partial = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }); const extensionLocation = await aUserExtension(manifest); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanOneOrMultipleExtensions(extensionLocation, ExtensionType.User, {}); @@ -122,7 +123,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan multiple extensions', async () => { const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanOneOrMultipleExtensions(dirname(extensionLocation), ExtensionType.User, {}); @@ -134,7 +135,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan user extension with different versions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' })); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanUserExtensions({}); @@ -146,7 +147,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan user extension include all versions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' })); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanUserExtensions({ includeAllVersions: true }); @@ -160,7 +161,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan user extension with different versions and higher version is not compatible', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2', engines: { vscode: '^1.67.0' } })); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanUserExtensions({}); @@ -172,7 +173,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan exclude invalid extensions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanUserExtensions({}); @@ -184,7 +185,7 @@ suite('NativeExtensionsScanerService Test', () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanUserExtensions({}); @@ -196,7 +197,7 @@ suite('NativeExtensionsScanerService Test', () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanUserExtensions({ includeUninstalled: true }); @@ -208,7 +209,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan include invalid extensions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanUserExtensions({ includeInvalid: true }); @@ -229,7 +230,7 @@ suite('NativeExtensionsScanerService Test', () => { const extensionLocation = await anExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }), joinPath(ROOT, 'additional')); await aSystemExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); await instantiationService.get(IFileService).writeFile(joinPath(instantiationService.get(INativeEnvironmentService).userHome, '.vscode-oss-dev', 'extensions', 'control.json'), VSBuffer.fromString(JSON.stringify({ 'pub.name2': 'disabled', 'pub.name': extensionLocation.fsPath }))); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanSystemExtensions({ checkControlFile: true }); @@ -241,7 +242,7 @@ suite('NativeExtensionsScanerService Test', () => { test('scan extension with default nls replacements', async () => { const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' })); await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' }))); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanUserExtensions({}); @@ -255,7 +256,7 @@ suite('NativeExtensionsScanerService Test', () => { await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' }))); const nlsLocation = joinPath(extensionLocation, 'package.en.json'); await instantiationService.get(IFileService).writeFile(nlsLocation, VSBuffer.fromString(JSON.stringify({ contents: { package: { displayName: 'Hello World EN' } } }))); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanUserExtensions({ nlsConfiguration: { locale: 'en', devMode: false, pseudo: false, translations: { 'pub.name': nlsLocation.fsPath } } }); @@ -269,7 +270,7 @@ suite('NativeExtensionsScanerService Test', () => { await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' }))); const nlsLocation = joinPath(extensionLocation, 'package.en.json'); await instantiationService.get(IFileService).writeFile(nlsLocation, VSBuffer.fromString(JSON.stringify({ contents: { package: { displayName: 'Hello World EN' } } }))); - const testObject: INativeExtensionsScannerService = instantiationService.createInstance(NativeExtensionsScannerService); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); const actual = await testObject.scanUserExtensions({ nlsConfiguration: { locale: 'en', devMode: false, pseudo: false, translations: { 'pub.name2': nlsLocation.fsPath } } }); diff --git a/src/vs/server/node/remoteAgentEnvironmentImpl.ts b/src/vs/server/node/remoteAgentEnvironmentImpl.ts index 28dda15bd92..5defb12624a 100644 --- a/src/vs/server/node/remoteAgentEnvironmentImpl.ts +++ b/src/vs/server/node/remoteAgentEnvironmentImpl.ts @@ -28,7 +28,7 @@ import { cwd } from 'vs/base/common/process'; import * as pfs from 'vs/base/node/pfs'; import { ServerConnectionToken, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken'; import { IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; -import { INativeExtensionsScannerService, NlsConfiguration, toExtensionDescription, Translations } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { IExtensionsScannerService, NlsConfiguration, toExtensionDescription, Translations } from 'vs/platform/extensionManagement/common/extensionsScannerService'; export class RemoteAgentEnvironmentChannel implements IServerChannel { @@ -42,7 +42,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { extensionManagementCLIService: IExtensionManagementCLIService, private readonly _logService: ILogService, private readonly _extensionHostStatusService: IExtensionHostStatusService, - private readonly _extensionsScannerService: INativeExtensionsScannerService, + private readonly _extensionsScannerService: IExtensionsScannerService, ) { if (_environmentService.args['install-builtin-extension']) { const installOptions: InstallOptions = { isMachineScoped: !!_environmentService.args['do-not-sync'], installPreReleaseVersion: !!_environmentService.args['pre-release'] }; diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index cf8abddae85..919924f53ad 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -68,7 +68,8 @@ import { REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/comm import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/workbench/services/remote/common/remoteFileSystemProviderClient'; import { ExtensionHostStatusService, IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; -import { NativeExtensionsScannerService, INativeExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService'; const eventPrefix = 'monacoworkbench'; @@ -153,7 +154,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken const downloadChannel = socketServer.getChannel('download', router); services.set(IDownloadService, new DownloadServiceChannelClient(downloadChannel, () => getUriTransformer('renderer') /* TODO: @Sandy @Joao need dynamic context based router */)); - services.set(INativeExtensionsScannerService, new SyncDescriptor(NativeExtensionsScannerService)); + services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); const instantiationService: IInstantiationService = new InstantiationService(services); @@ -178,7 +179,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken instantiationService.invokeFunction(accessor => { const extensionManagementService = accessor.get(IExtensionManagementService); - const extensionsScannerService = accessor.get(INativeExtensionsScannerService); + const extensionsScannerService = accessor.get(IExtensionsScannerService); const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, extensionManagementCLIService, logService, extensionHostStatusService, extensionsScannerService); socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel); diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionsScannerService.ts new file mode 100644 index 00000000000..5fa1a022246 --- /dev/null +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionsScannerService.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { joinPath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { AbstractExtensionsScannerService, IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; + +export class ExtensionsScannerService extends AbstractExtensionsScannerService implements IExtensionsScannerService { + + readonly systemExtensionsLocation: URI; + readonly userExtensionsLocation: URI; + protected readonly extensionsControlLocation: URI; + + constructor( + @IFileService fileService: IFileService, + @ILogService logService: ILogService, + @INativeEnvironmentService environmentService: INativeWorkbenchEnvironmentService, + @IProductService productService: IProductService, + ) { + super(fileService, logService, environmentService, productService); + this.systemExtensionsLocation = URI.file(environmentService.builtinExtensionsPath); + this.userExtensionsLocation = URI.file(environmentService.extensionsPath); + this.extensionsControlLocation = joinPath(environmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json'); + } + +} + +registerSingleton(IExtensionsScannerService, ExtensionsScannerService); diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index 47403cf5f56..6357aee8c51 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -17,7 +17,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { IFileService } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; -import { INativeExtensionsScannerService, IScannedExtension, NlsConfiguration, toExtensionDescription, Translations } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { IExtensionsScannerService, IScannedExtension, NlsConfiguration, toExtensionDescription, Translations } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ILogService } from 'vs/platform/log/common/log'; interface IExtensionCacheData { @@ -81,7 +81,7 @@ export class CachedExtensionScanner { @IHostService private readonly _hostService: IHostService, @IProductService private readonly _productService: IProductService, @IFileService private readonly _fileService: IFileService, - @INativeExtensionsScannerService private readonly _extensionsScannerService: INativeExtensionsScannerService, + @IExtensionsScannerService private readonly _extensionsScannerService: IExtensionsScannerService, @ILogService private readonly _logService: ILogService, ) { this.scannedExtensions = new Promise((resolve, reject) => { diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index deec2104b46..dfc2bcadef0 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -76,7 +76,6 @@ import 'vs/workbench/services/commands/common/commandService'; import 'vs/workbench/services/themes/browser/workbenchThemeService'; import 'vs/workbench/services/label/common/labelService'; import 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; -import 'vs/platform/extensionManagement/common/extensionsScannerService'; import 'vs/workbench/services/extensionManagement/browser/webExtensionsScannerService'; import 'vs/workbench/services/extensionManagement/browser/extensionEnablementService'; import 'vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService'; diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index 2150dd3f14b..148a706dfdb 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -61,6 +61,7 @@ import 'vs/workbench/services/encryption/electron-sandbox/encryptionService'; import 'vs/workbench/services/localizations/electron-sandbox/localizationsService'; import 'vs/workbench/services/telemetry/electron-sandbox/telemetryService'; import 'vs/workbench/services/extensions/electron-sandbox/extensionHostStarter'; +import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionsScannerService'; import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService'; import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionTipsService'; import 'vs/workbench/services/userDataSync/electron-sandbox/userDataSyncMachinesService'; -- cgit v1.2.3 From 811917d3e416a907849fe71995d3be4deab229ca Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 1 Apr 2022 23:26:24 +0530 Subject: change validations to tuple array --- .../common/extensionsScannerService.ts | 8 ++--- .../extensions/common/extensionValidator.ts | 38 +++++++++++----------- src/vs/platform/extensions/common/extensions.ts | 2 +- .../extensions/browser/extensionsActions.ts | 2 +- .../browser/webExtensionsScannerService.ts | 6 ++-- 5 files changed, 28 insertions(+), 28 deletions(-) (limited to 'src/vs') diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 3db83fd9409..91ddd186a1a 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -39,7 +39,7 @@ interface IRelaxedScannedExtension { targetPlatform: TargetPlatform; metadata: Metadata | undefined; isValid: boolean; - validations: readonly { readonly severity: Severity; readonly message: string }[]; + validations: readonly [Severity, string][]; } export type IScannedExtension = Readonly & { manifest: IExtensionManifest }; @@ -382,10 +382,10 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem const isBuiltin = type === ExtensionType.System || !!metadata?.isBuiltin; const validations = validateExtensionManifest(this.productService.version, this.productService.date, extensionLocation, manifest, isBuiltin); let isValid = true; - for (const validation of validations) { - if (validation.severity === Severity.Error) { + for (const [severity, message] of validations) { + if (severity === Severity.Error) { isValid = false; - this.logService.error(this.formatMessage(extensionLocation, validation.message)); + this.logService.error(this.formatMessage(extensionLocation, message)); } } return { diff --git a/src/vs/platform/extensions/common/extensionValidator.ts b/src/vs/platform/extensions/common/extensionValidator.ts index 628ecbacc83..c36ca616ae2 100644 --- a/src/vs/platform/extensions/common/extensionValidator.ts +++ b/src/vs/platform/extensions/common/extensionValidator.ts @@ -239,85 +239,85 @@ export function isValidVersion(_inputVersion: string | INormalizedVersion, _inpu type ProductDate = string | Date | undefined; -export function validateExtensionManifest(productVersion: string, productDate: ProductDate, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean): { severity: Severity; message: string }[] { - const validations: { severity: Severity; message: string }[] = []; +export function validateExtensionManifest(productVersion: string, productDate: ProductDate, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean): readonly [Severity, string][] { + const validations: [Severity, string][] = []; if (typeof extensionManifest.publisher !== 'undefined' && typeof extensionManifest.publisher !== 'string') { - validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.publisher', "property publisher must be of type `string`.") }); + validations.push([Severity.Error, nls.localize('extensionDescription.publisher', "property publisher must be of type `string`.")]); return validations; } if (typeof extensionManifest.name !== 'string') { - validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.name', "property `{0}` is mandatory and must be of type `string`", 'name') }); + validations.push([Severity.Error, nls.localize('extensionDescription.name', "property `{0}` is mandatory and must be of type `string`", 'name')]); return validations; } if (typeof extensionManifest.version !== 'string') { - validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.version', "property `{0}` is mandatory and must be of type `string`", 'version') }); + validations.push([Severity.Error, nls.localize('extensionDescription.version', "property `{0}` is mandatory and must be of type `string`", 'version')]); return validations; } if (!extensionManifest.engines) { - validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.engines', "property `{0}` is mandatory and must be of type `object`", 'engines') }); + validations.push([Severity.Error, nls.localize('extensionDescription.engines', "property `{0}` is mandatory and must be of type `object`", 'engines')]); return validations; } if (typeof extensionManifest.engines.vscode !== 'string') { - validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.engines.vscode', "property `{0}` is mandatory and must be of type `string`", 'engines.vscode') }); + validations.push([Severity.Error, nls.localize('extensionDescription.engines.vscode', "property `{0}` is mandatory and must be of type `string`", 'engines.vscode')]); return validations; } if (typeof extensionManifest.extensionDependencies !== 'undefined') { if (!isStringArray(extensionManifest.extensionDependencies)) { - validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.extensionDependencies', "property `{0}` can be omitted or must be of type `string[]`", 'extensionDependencies') }); + validations.push([Severity.Error, nls.localize('extensionDescription.extensionDependencies', "property `{0}` can be omitted or must be of type `string[]`", 'extensionDependencies')]); return validations; } } if (typeof extensionManifest.activationEvents !== 'undefined') { if (!isStringArray(extensionManifest.activationEvents)) { - validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.activationEvents1', "property `{0}` can be omitted or must be of type `string[]`", 'activationEvents') }); + validations.push([Severity.Error, nls.localize('extensionDescription.activationEvents1', "property `{0}` can be omitted or must be of type `string[]`", 'activationEvents')]); return validations; } if (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined') { - validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.activationEvents2', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main') }); + validations.push([Severity.Error, nls.localize('extensionDescription.activationEvents2', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main')]); return validations; } } if (typeof extensionManifest.extensionKind !== 'undefined') { if (typeof extensionManifest.main === 'undefined') { - validations.push({ severity: Severity.Warning, message: nls.localize('extensionDescription.extensionKind', "property `{0}` can be defined only if property `main` is also defined.", 'extensionKind') }); + validations.push([Severity.Warning, nls.localize('extensionDescription.extensionKind', "property `{0}` can be defined only if property `main` is also defined.", 'extensionKind')]); // not a failure case } } if (typeof extensionManifest.main !== 'undefined') { if (typeof extensionManifest.main !== 'string') { - validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main') }); + validations.push([Severity.Error, nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main')]); return validations; } else { const mainLocation = joinPath(extensionLocation, extensionManifest.main); if (!isEqualOrParent(mainLocation, extensionLocation)) { - validations.push({ severity: Severity.Warning, message: nls.localize('extensionDescription.main2', "Expected `main` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", mainLocation.path, extensionLocation.path) }); + validations.push([Severity.Warning, nls.localize('extensionDescription.main2', "Expected `main` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", mainLocation.path, extensionLocation.path)]); // not a failure case } } if (typeof extensionManifest.activationEvents === 'undefined') { - validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.main3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main') }); + validations.push([Severity.Error, nls.localize('extensionDescription.main3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main')]); return validations; } } if (typeof extensionManifest.browser !== 'undefined') { if (typeof extensionManifest.browser !== 'string') { - validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.browser1', "property `{0}` can be omitted or must be of type `string`", 'browser') }); + validations.push([Severity.Error, nls.localize('extensionDescription.browser1', "property `{0}` can be omitted or must be of type `string`", 'browser')]); return validations; } else { const browserLocation = joinPath(extensionLocation, extensionManifest.browser); if (!isEqualOrParent(browserLocation, extensionLocation)) { - validations.push({ severity: Severity.Warning, message: nls.localize('extensionDescription.browser2', "Expected `browser` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", browserLocation.path, extensionLocation.path) }); + validations.push([Severity.Warning, nls.localize('extensionDescription.browser2', "Expected `browser` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", browserLocation.path, extensionLocation.path)]); // not a failure case } } if (typeof extensionManifest.activationEvents === 'undefined') { - validations.push({ severity: Severity.Error, message: nls.localize('extensionDescription.browser3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'browser') }); + validations.push([Severity.Error, nls.localize('extensionDescription.browser3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'browser')]); return validations; } } if (!semver.valid(extensionManifest.version)) { - validations.push({ severity: Severity.Error, message: nls.localize('notSemver', "Extension version is not semver compatible.") }); + validations.push([Severity.Error, nls.localize('notSemver', "Extension version is not semver compatible.")]); return validations; } @@ -325,7 +325,7 @@ export function validateExtensionManifest(productVersion: string, productDate: P const isValid = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices); if (!isValid) { for (const notice of notices) { - validations.push({ severity: Severity.Error, message: notice }); + validations.push([Severity.Error, notice]); } } return validations; diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index eda46215228..5ee57cacb6b 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -322,7 +322,7 @@ export interface IExtension { readonly readmeUrl?: URI; readonly changelogUrl?: URI; readonly isValid: boolean; - readonly validations: readonly { readonly severity: Severity; readonly message: string }[]; + readonly validations: readonly [Severity, string][]; } /** diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 31f4499b9de..ad81be89e2b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -2349,7 +2349,7 @@ export class ExtensionStatusAction extends ExtensionAction { } if (isEnabled && !isRunning && !this.extension.local.isValid) { - const errors = this.extension.local.validations.filter(v => v.severity === Severity.Error).map(v => v.message); + const errors = this.extension.local.validations.filter(([severity]) => severity === Severity.Error).map(([, message]) => message); this.updateStatus({ icon: errorIcon, message: new MarkdownString(errors.join(' ').trim()) }, true); } diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index ace28ce128f..038bfa35339 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -577,10 +577,10 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const validations = validateExtensionManifest(this.productService.version, this.productService.date, webExtension.location, manifest, false); let isValid = true; - for (const validation of validations) { - if (validation.severity === Severity.Error) { + for (const [severity, message] of validations) { + if (severity === Severity.Error) { isValid = false; - this.logService.error(validation.message); + this.logService.error(message); } } -- cgit v1.2.3 From 9ecfc9365fcbafe3538b2c978f2e18be951c4c79 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 1 Apr 2022 23:30:54 +0530 Subject: fix layer check --- .../platform/extensionManagement/common/extensionsScannerService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/vs') diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 91ddd186a1a..f5ac106af57 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -18,7 +18,7 @@ import Severity from 'vs/base/common/severity'; import { isArray, isObject, isString } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -136,7 +136,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem constructor( @IFileService private readonly fileService: IFileService, @ILogService private readonly logService: ILogService, - @INativeEnvironmentService private readonly environmentService: IEnvironmentService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, @IProductService private readonly productService: IProductService, ) { super(); -- cgit v1.2.3 From 68a923d383a7e63fbd02c735610255811f354888 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 4 Apr 2022 10:56:20 +0200 Subject: Self register configuration resolver for web Part of #146027 --- .../browser/baseConfigurationResolverService.ts | 369 +++++++++++++++++++++ .../browser/configurationResolverService.ts | 369 +-------------------- .../configurationResolverService.ts | 2 +- .../configurationResolverService.test.ts | 2 +- src/vs/workbench/workbench.web.main.ts | 4 +- 5 files changed, 380 insertions(+), 366 deletions(-) create mode 100644 src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts (limited to 'src/vs') diff --git a/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts new file mode 100644 index 00000000000..f6f0d6fa353 --- /dev/null +++ b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts @@ -0,0 +1,369 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { URI as uri } from 'vs/base/common/uri'; +import * as nls from 'vs/nls'; +import * as Types from 'vs/base/common/types'; +import { Schemas } from 'vs/base/common/network'; +import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor'; +import { IStringDictionary, forEach, fromMap } from 'vs/base/common/collections'; +import { IConfigurationService, IConfigurationOverrides, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IWorkspaceFolder, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { IQuickInputService, IInputOptions, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput'; +import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IProcessEnvironment } from 'vs/base/common/platform'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; + +export abstract class BaseConfigurationResolverService extends AbstractVariableResolverService { + + static readonly INPUT_OR_COMMAND_VARIABLES_PATTERN = /\${((input|command):(.*?))}/g; + + constructor( + context: { + getAppRoot: () => string | undefined; + getExecPath: () => string | undefined; + }, + envVariablesPromise: Promise, + editorService: IEditorService, + private readonly configurationService: IConfigurationService, + private readonly commandService: ICommandService, + private readonly workspaceContextService: IWorkspaceContextService, + private readonly quickInputService: IQuickInputService, + private readonly labelService: ILabelService, + private readonly pathService: IPathService + ) { + super({ + getFolderUri: (folderName: string): uri | undefined => { + const folder = workspaceContextService.getWorkspace().folders.filter(f => f.name === folderName).pop(); + return folder ? folder.uri : undefined; + }, + getWorkspaceFolderCount: (): number => { + return workspaceContextService.getWorkspace().folders.length; + }, + getConfigurationValue: (folderUri: uri | undefined, suffix: string): string | undefined => { + return configurationService.getValue(suffix, folderUri ? { resource: folderUri } : {}); + }, + getAppRoot: (): string | undefined => { + return context.getAppRoot(); + }, + getExecPath: (): string | undefined => { + return context.getExecPath(); + }, + getFilePath: (): string | undefined => { + const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { + supportSideBySide: SideBySideEditor.PRIMARY, + filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme] + }); + if (!fileResource) { + return undefined; + } + return this.labelService.getUriLabel(fileResource, { noPrefix: true }); + }, + getWorkspaceFolderPathForFile: (): string | undefined => { + const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { + supportSideBySide: SideBySideEditor.PRIMARY, + filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme] + }); + if (!fileResource) { + return undefined; + } + const wsFolder = workspaceContextService.getWorkspaceFolder(fileResource); + if (!wsFolder) { + return undefined; + } + return this.labelService.getUriLabel(wsFolder.uri, { noPrefix: true }); + }, + getSelectedText: (): string | undefined => { + const activeTextEditorControl = editorService.activeTextEditorControl; + + let activeControl: ICodeEditor | null = null; + + if (isCodeEditor(activeTextEditorControl)) { + activeControl = activeTextEditorControl; + } else if (isDiffEditor(activeTextEditorControl)) { + const original = activeTextEditorControl.getOriginalEditor(); + const modified = activeTextEditorControl.getModifiedEditor(); + activeControl = original.hasWidgetFocus() ? original : modified; + } + + const activeModel = activeControl?.getModel(); + const activeSelection = activeControl?.getSelection(); + if (activeModel && activeSelection) { + return activeModel.getValueInRange(activeSelection); + } + return undefined; + }, + getLineNumber: (): string | undefined => { + const activeTextEditorControl = editorService.activeTextEditorControl; + if (isCodeEditor(activeTextEditorControl)) { + const selection = activeTextEditorControl.getSelection(); + if (selection) { + const lineNumber = selection.positionLineNumber; + return String(lineNumber); + } + } + return undefined; + } + }, labelService, pathService.userHome().then(home => home.path), envVariablesPromise); + } + + public override async resolveWithInteractionReplace(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary, target?: ConfigurationTarget): Promise { + // resolve any non-interactive variables and any contributed variables + config = await this.resolveAnyAsync(folder, config); + + // resolve input variables in the order in which they are encountered + return this.resolveWithInteraction(folder, config, section, variables, target).then(mapping => { + // finally substitute evaluated command variables (if there are any) + if (!mapping) { + return null; + } else if (mapping.size > 0) { + return this.resolveAnyAsync(folder, config, fromMap(mapping)); + } else { + return config; + } + }); + } + + public override async resolveWithInteraction(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary, target?: ConfigurationTarget): Promise | undefined> { + // resolve any non-interactive variables and any contributed variables + const resolved = await this.resolveAnyMap(folder, config); + config = resolved.newConfig; + const allVariableMapping: Map = resolved.resolvedVariables; + + // resolve input and command variables in the order in which they are encountered + return this.resolveWithInputAndCommands(folder, config, variables, section, target).then(inputOrCommandMapping => { + if (this.updateMapping(inputOrCommandMapping, allVariableMapping)) { + return allVariableMapping; + } + return undefined; + }); + } + + /** + * Add all items from newMapping to fullMapping. Returns false if newMapping is undefined. + */ + private updateMapping(newMapping: IStringDictionary | undefined, fullMapping: Map): boolean { + if (!newMapping) { + return false; + } + forEach(newMapping, (entry) => { + fullMapping.set(entry.key, entry.value); + }); + return true; + } + + /** + * Finds and executes all input and command variables in the given configuration and returns their values as a dictionary. + * Please note: this method does not substitute the input or command variables (so the configuration is not modified). + * The returned dictionary can be passed to "resolvePlatform" for the actual substitution. + * See #6569. + * + * @param variableToCommandMap Aliases for commands + */ + private async resolveWithInputAndCommands(folder: IWorkspaceFolder | undefined, configuration: any, variableToCommandMap?: IStringDictionary, section?: string, target?: ConfigurationTarget): Promise | undefined> { + + if (!configuration) { + return Promise.resolve(undefined); + } + + // get all "inputs" + let inputs: ConfiguredInput[] = []; + if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) { + const overrides: IConfigurationOverrides = folder ? { resource: folder.uri } : {}; + let result = this.configurationService.inspect(section, overrides); + if (result && (result.userValue || result.workspaceValue || result.workspaceFolderValue)) { + switch (target) { + case ConfigurationTarget.USER: inputs = (result.userValue)?.inputs; break; + case ConfigurationTarget.WORKSPACE: inputs = (result.workspaceValue)?.inputs; break; + default: inputs = (result.workspaceFolderValue)?.inputs; + } + } else { + const valueResult = this.configurationService.getValue(section, overrides); + if (valueResult) { + inputs = valueResult.inputs; + } + } + } + + // extract and dedupe all "input" and "command" variables and preserve their order in an array + const variables: string[] = []; + this.findVariables(configuration, variables); + + const variableValues: IStringDictionary = Object.create(null); + + for (const variable of variables) { + + const [type, name] = variable.split(':', 2); + + let result: string | undefined; + + switch (type) { + + case 'input': + result = await this.showUserInput(name, inputs); + break; + + case 'command': { + // use the name as a command ID #12735 + const commandId = (variableToCommandMap ? variableToCommandMap[name] : undefined) || name; + result = await this.commandService.executeCommand(commandId, configuration); + if (typeof result !== 'string' && !Types.isUndefinedOrNull(result)) { + throw new Error(nls.localize('commandVariable.noStringType', "Cannot substitute command variable '{0}' because command did not return a result of type string.", commandId)); + } + break; + } + default: + // Try to resolve it as a contributed variable + if (this._contributedVariables.has(variable)) { + result = await this._contributedVariables.get(variable)!(); + } + } + + if (typeof result === 'string') { + variableValues[variable] = result; + } else { + return undefined; + } + } + + return variableValues; + } + + /** + * Recursively finds all command or input variables in object and pushes them into variables. + * @param object object is searched for variables. + * @param variables All found variables are returned in variables. + */ + private findVariables(object: any, variables: string[]) { + if (typeof object === 'string') { + let matches; + while ((matches = BaseConfigurationResolverService.INPUT_OR_COMMAND_VARIABLES_PATTERN.exec(object)) !== null) { + if (matches.length === 4) { + const command = matches[1]; + if (variables.indexOf(command) < 0) { + variables.push(command); + } + } + } + this._contributedVariables.forEach((value, contributed: string) => { + if ((variables.indexOf(contributed) < 0) && (object.indexOf('${' + contributed + '}') >= 0)) { + variables.push(contributed); + } + }); + } else if (Types.isArray(object)) { + object.forEach(value => { + this.findVariables(value, variables); + }); + } else if (object) { + Object.keys(object).forEach(key => { + const value = object[key]; + this.findVariables(value, variables); + }); + } + } + + /** + * Takes the provided input info and shows the quick pick so the user can provide the value for the input + * @param variable Name of the input variable. + * @param inputInfos Information about each possible input variable. + */ + private showUserInput(variable: string, inputInfos: ConfiguredInput[]): Promise { + + if (!inputInfos) { + return Promise.reject(new Error(nls.localize('inputVariable.noInputSection', "Variable '{0}' must be defined in an '{1}' section of the debug or task configuration.", variable, 'input'))); + } + + // find info for the given input variable + const info = inputInfos.filter(item => item.id === variable).pop(); + if (info) { + + const missingAttribute = (attrName: string) => { + throw new Error(nls.localize('inputVariable.missingAttribute', "Input variable '{0}' is of type '{1}' and must include '{2}'.", variable, info.type, attrName)); + }; + + switch (info.type) { + + case 'promptString': { + if (!Types.isString(info.description)) { + missingAttribute('description'); + } + const inputOptions: IInputOptions = { prompt: info.description, ignoreFocusLost: true }; + if (info.default) { + inputOptions.value = info.default; + } + if (info.password) { + inputOptions.password = info.password; + } + return this.quickInputService.input(inputOptions).then(resolvedInput => { + return resolvedInput; + }); + } + + case 'pickString': { + if (!Types.isString(info.description)) { + missingAttribute('description'); + } + if (Types.isArray(info.options)) { + info.options.forEach(pickOption => { + if (!Types.isString(pickOption) && !Types.isString(pickOption.value)) { + missingAttribute('value'); + } + }); + } else { + missingAttribute('options'); + } + interface PickStringItem extends IQuickPickItem { + value: string; + } + const picks = new Array(); + info.options.forEach(pickOption => { + const value = Types.isString(pickOption) ? pickOption : pickOption.value; + const label = Types.isString(pickOption) ? undefined : pickOption.label; + + // If there is no label defined, use value as label + const item: PickStringItem = { + label: label ? `${label}: ${value}` : value, + value: value + }; + + if (value === info.default) { + item.description = nls.localize('inputVariable.defaultInputValue', "(Default)"); + picks.unshift(item); + } else { + picks.push(item); + } + }); + const pickOptions: IPickOptions = { placeHolder: info.description, matchOnDetail: true, ignoreFocusLost: true }; + return this.quickInputService.pick(picks, pickOptions, undefined).then(resolvedInput => { + if (resolvedInput) { + return resolvedInput.value; + } + return undefined; + }); + } + + case 'command': { + if (!Types.isString(info.command)) { + missingAttribute('command'); + } + return this.commandService.executeCommand(info.command, info.args).then(result => { + if (typeof result === 'string' || Types.isUndefinedOrNull(result)) { + return result; + } + throw new Error(nls.localize('inputVariable.command.noStringType', "Cannot substitute input variable '{0}' because command '{1}' did not return a result of type string.", variable, info.command)); + }); + } + + default: + throw new Error(nls.localize('inputVariable.unknownType', "Input variable '{0}' can only be of type 'promptString', 'pickString', or 'command'.", variable)); + } + } + return Promise.reject(new Error(nls.localize('inputVariable.undefinedVariable', "Undefined input variable '{0}' encountered. Remove or define '{0}' to continue.", variable))); + } +} diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index acbac82cf51..c4de8509832 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -3,371 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI as uri } from 'vs/base/common/uri'; -import * as nls from 'vs/nls'; -import * as Types from 'vs/base/common/types'; -import { Schemas } from 'vs/base/common/network'; -import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor'; -import { IStringDictionary, forEach, fromMap } from 'vs/base/common/collections'; -import { IConfigurationService, IConfigurationOverrides, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IWorkspaceFolder, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; -import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { IQuickInputService, IInputOptions, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput'; -import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { IProcessEnvironment } from 'vs/base/common/platform'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { ILabelService } from 'vs/platform/label/common/label'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; - -export abstract class BaseConfigurationResolverService extends AbstractVariableResolverService { - - static readonly INPUT_OR_COMMAND_VARIABLES_PATTERN = /\${((input|command):(.*?))}/g; - - constructor( - context: { - getAppRoot: () => string | undefined; - getExecPath: () => string | undefined; - }, - envVariablesPromise: Promise, - editorService: IEditorService, - private readonly configurationService: IConfigurationService, - private readonly commandService: ICommandService, - private readonly workspaceContextService: IWorkspaceContextService, - private readonly quickInputService: IQuickInputService, - private readonly labelService: ILabelService, - private readonly pathService: IPathService - ) { - super({ - getFolderUri: (folderName: string): uri | undefined => { - const folder = workspaceContextService.getWorkspace().folders.filter(f => f.name === folderName).pop(); - return folder ? folder.uri : undefined; - }, - getWorkspaceFolderCount: (): number => { - return workspaceContextService.getWorkspace().folders.length; - }, - getConfigurationValue: (folderUri: uri | undefined, suffix: string): string | undefined => { - return configurationService.getValue(suffix, folderUri ? { resource: folderUri } : {}); - }, - getAppRoot: (): string | undefined => { - return context.getAppRoot(); - }, - getExecPath: (): string | undefined => { - return context.getExecPath(); - }, - getFilePath: (): string | undefined => { - const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { - supportSideBySide: SideBySideEditor.PRIMARY, - filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme] - }); - if (!fileResource) { - return undefined; - } - return this.labelService.getUriLabel(fileResource, { noPrefix: true }); - }, - getWorkspaceFolderPathForFile: (): string | undefined => { - const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { - supportSideBySide: SideBySideEditor.PRIMARY, - filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme] - }); - if (!fileResource) { - return undefined; - } - const wsFolder = workspaceContextService.getWorkspaceFolder(fileResource); - if (!wsFolder) { - return undefined; - } - return this.labelService.getUriLabel(wsFolder.uri, { noPrefix: true }); - }, - getSelectedText: (): string | undefined => { - const activeTextEditorControl = editorService.activeTextEditorControl; - - let activeControl: ICodeEditor | null = null; - - if (isCodeEditor(activeTextEditorControl)) { - activeControl = activeTextEditorControl; - } else if (isDiffEditor(activeTextEditorControl)) { - const original = activeTextEditorControl.getOriginalEditor(); - const modified = activeTextEditorControl.getModifiedEditor(); - activeControl = original.hasWidgetFocus() ? original : modified; - } - - const activeModel = activeControl?.getModel(); - const activeSelection = activeControl?.getSelection(); - if (activeModel && activeSelection) { - return activeModel.getValueInRange(activeSelection); - } - return undefined; - }, - getLineNumber: (): string | undefined => { - const activeTextEditorControl = editorService.activeTextEditorControl; - if (isCodeEditor(activeTextEditorControl)) { - const selection = activeTextEditorControl.getSelection(); - if (selection) { - const lineNumber = selection.positionLineNumber; - return String(lineNumber); - } - } - return undefined; - } - }, labelService, pathService.userHome().then(home => home.path), envVariablesPromise); - } - - public override async resolveWithInteractionReplace(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary, target?: ConfigurationTarget): Promise { - // resolve any non-interactive variables and any contributed variables - config = await this.resolveAnyAsync(folder, config); - - // resolve input variables in the order in which they are encountered - return this.resolveWithInteraction(folder, config, section, variables, target).then(mapping => { - // finally substitute evaluated command variables (if there are any) - if (!mapping) { - return null; - } else if (mapping.size > 0) { - return this.resolveAnyAsync(folder, config, fromMap(mapping)); - } else { - return config; - } - }); - } - - public override async resolveWithInteraction(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary, target?: ConfigurationTarget): Promise | undefined> { - // resolve any non-interactive variables and any contributed variables - const resolved = await this.resolveAnyMap(folder, config); - config = resolved.newConfig; - const allVariableMapping: Map = resolved.resolvedVariables; - - // resolve input and command variables in the order in which they are encountered - return this.resolveWithInputAndCommands(folder, config, variables, section, target).then(inputOrCommandMapping => { - if (this.updateMapping(inputOrCommandMapping, allVariableMapping)) { - return allVariableMapping; - } - return undefined; - }); - } - - /** - * Add all items from newMapping to fullMapping. Returns false if newMapping is undefined. - */ - private updateMapping(newMapping: IStringDictionary | undefined, fullMapping: Map): boolean { - if (!newMapping) { - return false; - } - forEach(newMapping, (entry) => { - fullMapping.set(entry.key, entry.value); - }); - return true; - } - - /** - * Finds and executes all input and command variables in the given configuration and returns their values as a dictionary. - * Please note: this method does not substitute the input or command variables (so the configuration is not modified). - * The returned dictionary can be passed to "resolvePlatform" for the actual substitution. - * See #6569. - * - * @param variableToCommandMap Aliases for commands - */ - private async resolveWithInputAndCommands(folder: IWorkspaceFolder | undefined, configuration: any, variableToCommandMap?: IStringDictionary, section?: string, target?: ConfigurationTarget): Promise | undefined> { - - if (!configuration) { - return Promise.resolve(undefined); - } - - // get all "inputs" - let inputs: ConfiguredInput[] = []; - if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) { - const overrides: IConfigurationOverrides = folder ? { resource: folder.uri } : {}; - let result = this.configurationService.inspect(section, overrides); - if (result && (result.userValue || result.workspaceValue || result.workspaceFolderValue)) { - switch (target) { - case ConfigurationTarget.USER: inputs = (result.userValue)?.inputs; break; - case ConfigurationTarget.WORKSPACE: inputs = (result.workspaceValue)?.inputs; break; - default: inputs = (result.workspaceFolderValue)?.inputs; - } - } else { - const valueResult = this.configurationService.getValue(section, overrides); - if (valueResult) { - inputs = valueResult.inputs; - } - } - } - - // extract and dedupe all "input" and "command" variables and preserve their order in an array - const variables: string[] = []; - this.findVariables(configuration, variables); - - const variableValues: IStringDictionary = Object.create(null); - - for (const variable of variables) { - - const [type, name] = variable.split(':', 2); - - let result: string | undefined; - - switch (type) { - - case 'input': - result = await this.showUserInput(name, inputs); - break; - - case 'command': { - // use the name as a command ID #12735 - const commandId = (variableToCommandMap ? variableToCommandMap[name] : undefined) || name; - result = await this.commandService.executeCommand(commandId, configuration); - if (typeof result !== 'string' && !Types.isUndefinedOrNull(result)) { - throw new Error(nls.localize('commandVariable.noStringType', "Cannot substitute command variable '{0}' because command did not return a result of type string.", commandId)); - } - break; - } - default: - // Try to resolve it as a contributed variable - if (this._contributedVariables.has(variable)) { - result = await this._contributedVariables.get(variable)!(); - } - } - - if (typeof result === 'string') { - variableValues[variable] = result; - } else { - return undefined; - } - } - - return variableValues; - } - - /** - * Recursively finds all command or input variables in object and pushes them into variables. - * @param object object is searched for variables. - * @param variables All found variables are returned in variables. - */ - private findVariables(object: any, variables: string[]) { - if (typeof object === 'string') { - let matches; - while ((matches = BaseConfigurationResolverService.INPUT_OR_COMMAND_VARIABLES_PATTERN.exec(object)) !== null) { - if (matches.length === 4) { - const command = matches[1]; - if (variables.indexOf(command) < 0) { - variables.push(command); - } - } - } - this._contributedVariables.forEach((value, contributed: string) => { - if ((variables.indexOf(contributed) < 0) && (object.indexOf('${' + contributed + '}') >= 0)) { - variables.push(contributed); - } - }); - } else if (Types.isArray(object)) { - object.forEach(value => { - this.findVariables(value, variables); - }); - } else if (object) { - Object.keys(object).forEach(key => { - const value = object[key]; - this.findVariables(value, variables); - }); - } - } - - /** - * Takes the provided input info and shows the quick pick so the user can provide the value for the input - * @param variable Name of the input variable. - * @param inputInfos Information about each possible input variable. - */ - private showUserInput(variable: string, inputInfos: ConfiguredInput[]): Promise { - - if (!inputInfos) { - return Promise.reject(new Error(nls.localize('inputVariable.noInputSection', "Variable '{0}' must be defined in an '{1}' section of the debug or task configuration.", variable, 'input'))); - } - - // find info for the given input variable - const info = inputInfos.filter(item => item.id === variable).pop(); - if (info) { - - const missingAttribute = (attrName: string) => { - throw new Error(nls.localize('inputVariable.missingAttribute', "Input variable '{0}' is of type '{1}' and must include '{2}'.", variable, info.type, attrName)); - }; - - switch (info.type) { - - case 'promptString': { - if (!Types.isString(info.description)) { - missingAttribute('description'); - } - const inputOptions: IInputOptions = { prompt: info.description, ignoreFocusLost: true }; - if (info.default) { - inputOptions.value = info.default; - } - if (info.password) { - inputOptions.password = info.password; - } - return this.quickInputService.input(inputOptions).then(resolvedInput => { - return resolvedInput; - }); - } - - case 'pickString': { - if (!Types.isString(info.description)) { - missingAttribute('description'); - } - if (Types.isArray(info.options)) { - info.options.forEach(pickOption => { - if (!Types.isString(pickOption) && !Types.isString(pickOption.value)) { - missingAttribute('value'); - } - }); - } else { - missingAttribute('options'); - } - interface PickStringItem extends IQuickPickItem { - value: string; - } - const picks = new Array(); - info.options.forEach(pickOption => { - const value = Types.isString(pickOption) ? pickOption : pickOption.value; - const label = Types.isString(pickOption) ? undefined : pickOption.label; - - // If there is no label defined, use value as label - const item: PickStringItem = { - label: label ? `${label}: ${value}` : value, - value: value - }; - - if (value === info.default) { - item.description = nls.localize('inputVariable.defaultInputValue', "(Default)"); - picks.unshift(item); - } else { - picks.push(item); - } - }); - const pickOptions: IPickOptions = { placeHolder: info.description, matchOnDetail: true, ignoreFocusLost: true }; - return this.quickInputService.pick(picks, pickOptions, undefined).then(resolvedInput => { - if (resolvedInput) { - return resolvedInput.value; - } - return undefined; - }); - } - - case 'command': { - if (!Types.isString(info.command)) { - missingAttribute('command'); - } - return this.commandService.executeCommand(info.command, info.args).then(result => { - if (typeof result === 'string' || Types.isUndefinedOrNull(result)) { - return result; - } - throw new Error(nls.localize('inputVariable.command.noStringType', "Cannot substitute input variable '{0}' because command '{1}' did not return a result of type string.", variable, info.command)); - }); - } - - default: - throw new Error(nls.localize('inputVariable.unknownType', "Input variable '{0}' can only be of type 'promptString', 'pickString', or 'command'.", variable)); - } - } - return Promise.reject(new Error(nls.localize('inputVariable.undefinedVariable', "Undefined input variable '{0}' encountered. Remove or define '{0}' to continue.", variable))); - } -} +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService'; export class ConfigurationResolverService extends BaseConfigurationResolverService { @@ -385,3 +30,5 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi commandService, workspaceContextService, quickInputService, labelService, pathService); } } + +registerSingleton(IConfigurationResolverService, ConfigurationResolverService, true); diff --git a/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts index b98d3a68357..f4fcf389c87 100644 --- a/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts @@ -11,7 +11,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; +import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService'; import { ILabelService } from 'vs/platform/label/common/label'; import { IShellEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/shellEnvironmentService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index 014d9f42c43..ed179a4847b 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -19,7 +19,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { IFormatterChangeEvent, ILabelService, ResourceLabelFormatter } from 'vs/platform/label/common/label'; import { IWorkspace, IWorkspaceFolder, IWorkspaceIdentifier, Workspace } from 'vs/platform/workspace/common/workspace'; import { testWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; -import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; +import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 8f3d9020fd0..b4b1806ff41 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -61,6 +61,7 @@ import 'vs/workbench/services/tunnel/browser/tunnelService'; import 'vs/workbench/services/files/browser/elevatedFileService'; import 'vs/workbench/services/workingCopy/browser/workingCopyHistoryService'; import 'vs/workbench/services/userDataSync/browser/webUserDataSyncEnablementService'; +import 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; @@ -85,8 +86,6 @@ import { NullEndpointTelemetryService } from 'vs/platform/telemetry/common/telem import { ITitleService } from 'vs/workbench/services/title/common/titleService'; import { TitlebarPart } from 'vs/workbench/browser/parts/titlebar/titlebarPart'; import { ITimerService, TimerService } from 'vs/workbench/services/timer/browser/timerService'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { ConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import { IDiagnosticsService, NullDiagnosticsService } from 'vs/platform/diagnostics/common/diagnostics'; registerSingleton(IWorkbenchExtensionManagementService, ExtensionManagementService); @@ -102,7 +101,6 @@ registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); registerSingleton(ITitleService, TitlebarPart); registerSingleton(IExtensionTipsService, ExtensionTipsService); registerSingleton(ITimerService, TimerService); -registerSingleton(IConfigurationResolverService, ConfigurationResolverService, true); registerSingleton(ICustomEndpointTelemetryService, NullEndpointTelemetryService, true); registerSingleton(IDiagnosticsService, NullDiagnosticsService, true); -- cgit v1.2.3 From 145e5934e91295ef643026f5b48289fceb69575a Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 4 Apr 2022 11:15:21 +0200 Subject: Fixes #146434 --- src/vs/editor/contrib/multicursor/browser/multicursor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/vs') diff --git a/src/vs/editor/contrib/multicursor/browser/multicursor.ts b/src/vs/editor/contrib/multicursor/browser/multicursor.ts index 574a5be3e99..7c658f0fa9b 100644 --- a/src/vs/editor/contrib/multicursor/browser/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/browser/multicursor.ts @@ -1095,7 +1095,7 @@ export class FocusNextCursor extends EditorAction { constructor() { super({ id: 'editor.action.focusNextCursor', - label: nls.localize('mutlicursor.focusNextCursor', "Focus next cursor"), + label: nls.localize('mutlicursor.focusNextCursor', "Focus Next Cursor"), description: { description: nls.localize('mutlicursor.focusNextCursor.description', "Focuses the next cursor"), args: [], @@ -1134,7 +1134,7 @@ export class FocusPreviousCursor extends EditorAction { constructor() { super({ id: 'editor.action.focusPreviousCursor', - label: nls.localize('mutlicursor.focusPreviousCursor', "Focus previous cursor"), + label: nls.localize('mutlicursor.focusPreviousCursor', "Focus Previous Cursor"), description: { description: nls.localize('mutlicursor.focusPreviousCursor.description', "Focuses the previous cursor"), args: [], -- cgit v1.2.3 From d6e89ba74f5c36b765fd1a47fbc12a8ec441c070 Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 4 Apr 2022 11:36:26 +0200 Subject: check with suggest memory when doing inline suggestions, https://github.com/microsoft/vscode/issues/146531 --- .../contrib/suggest/browser/suggestInlineCompletions.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'src/vs') diff --git a/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts b/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts index 40358156aac..060b60d499d 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts @@ -5,6 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { FuzzyScore } from 'vs/base/common/filters'; +import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, RefCountedDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; @@ -20,6 +21,7 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat import { CompletionItemInsertTextRule } from 'vs/editor/common/standalone/standaloneEnums'; import { CompletionModel, LineContext } from 'vs/editor/contrib/suggest/browser/completionModel'; import { CompletionItemModel, provideSuggestionItems, QuickSuggestionsOptions } from 'vs/editor/contrib/suggest/browser/suggest'; +import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/browser/suggestMemory'; import { WordDistance } from 'vs/editor/contrib/suggest/browser/wordDistance'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -32,6 +34,7 @@ class InlineCompletionResults extends RefCountedDisposable implements InlineComp readonly word: IWordAtPosition, readonly completionModel: CompletionModel, completions: CompletionItemModel, + @ISuggestMemoryService private readonly _suggestMemoryService: ISuggestMemoryService, ) { super(completions.disposable); } @@ -44,7 +47,15 @@ class InlineCompletionResults extends RefCountedDisposable implements InlineComp get items(): InlineCompletion[] { const result: InlineCompletion[] = []; - for (const item of this.completionModel.items) { + + // Split items by preselected index. This ensures the memory-selected item shows first and that better/worst + // ranked items are before/after + const { items } = this.completionModel; + const selectedIndex = this._suggestMemoryService.select(this.model, { lineNumber: this.line, column: this.word.endColumn + this.completionModel.lineContext.characterCountDelta }, items); + const first = Iterable.slice(items, selectedIndex); + const second = Iterable.slice(items, 0, selectedIndex); + + for (const item of Iterable.concat(first, second)) { if (item.score === FuzzyScore.Default) { // skip items that have no overlap @@ -80,6 +91,7 @@ class SuggestInlineCompletions implements InlineCompletionsProvider(id: T, model: ITextModel) => FindComputedEditorOptionValueById, @ILanguageFeaturesService private readonly _languageFeatureService: ILanguageFeaturesService, @IClipboardService private readonly _clipboardService: IClipboardService, + @ISuggestMemoryService private readonly _suggestMemoryService: ISuggestMemoryService, ) { } async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise { @@ -143,7 +155,7 @@ class SuggestInlineCompletions implements InlineCompletionsProvider Date: Mon, 4 Apr 2022 17:16:54 +0530 Subject: Fix #144294 --- .../browser/extensionEnablementService.ts | 32 ++++++++++++++++++++++ .../browser/extensionEnablementService.test.ts | 31 +++++++++++++++++++-- 2 files changed, 60 insertions(+), 3 deletions(-) (limited to 'src/vs') diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index f57e3cb5db6..1040b35beff 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -182,6 +182,11 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } async setEnablement(extensions: IExtension[], newState: EnablementState): Promise { + await this.extensionsManager.whenInitialized(); + + if (newState === EnablementState.EnabledGlobally || newState === EnablementState.EnabledWorkspace) { + extensions.push(...this.getExtensionsToEnableRecursively(extensions, this.extensionsManager.extensions, newState, { dependencies: true, pack: true })); + } const workspace = newState === EnablementState.DisabledWorkspace || newState === EnablementState.EnabledWorkspace; for (const extension of extensions) { @@ -213,6 +218,33 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return result; } + private getExtensionsToEnableRecursively(extensions: IExtension[], allExtensions: ReadonlyArray, enablementState: EnablementState, options: { dependencies: boolean; pack: boolean }, checked: IExtension[] = []): IExtension[] { + const toCheck = extensions.filter(e => checked.indexOf(e) === -1); + if (toCheck.length) { + for (const extension of toCheck) { + checked.push(extension); + } + const extensionsToDisable = allExtensions.filter(i => { + if (checked.indexOf(i) !== -1) { + return false; + } + if (this.getEnablementState(i) === enablementState) { + return false; + } + return (options.dependencies || options.pack) + && extensions.some(extension => + (options.dependencies && extension.manifest.extensionDependencies?.some(id => areSameExtensions({ id }, i.identifier))) + || (options.pack && extension.manifest.extensionPack?.some(id => areSameExtensions({ id }, i.identifier))) + ); + }); + if (extensionsToDisable.length) { + extensionsToDisable.push(...this.getExtensionsToEnableRecursively(extensionsToDisable, allExtensions, enablementState, options, checked)); + } + return extensionsToDisable; + } + return []; + } + private _setUserEnablementState(extension: IExtension, newState: EnablementState): Promise { const currentState = this._getUserEnablementState(extension.identifier); diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 894fc37142d..660c2fcd2e7 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -386,6 +386,31 @@ suite('ExtensionEnablementService Test', () => { assert.strictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); }); + test('test enable an extension also enables dependencies', async () => { + installed.push(...[aLocalExtension2('pub.a', { extensionDependencies: ['pub.b'] }), aLocalExtension('pub.b')]); + const target = installed[0]; + const dep = installed[1]; + await (testObject).waitUntilInitialized(); + await testObject.setEnablement([dep, target], EnablementState.DisabledGlobally); + await testObject.setEnablement([target], EnablementState.EnabledGlobally); + assert.ok(testObject.isEnabled(target)); + assert.ok(testObject.isEnabled(dep)); + assert.strictEqual(testObject.getEnablementState(target), EnablementState.EnabledGlobally); + assert.strictEqual(testObject.getEnablementState(dep), EnablementState.EnabledGlobally); + }); + + test('test enable an extension also enables packed extensions', async () => { + installed.push(...[aLocalExtension2('pub.a', { extensionPack: ['pub.b'] }), aLocalExtension('pub.b')]); + const target = installed[0]; + const dep = installed[1]; + await testObject.setEnablement([dep, target], EnablementState.DisabledGlobally); + await testObject.setEnablement([target], EnablementState.EnabledGlobally); + assert.ok(testObject.isEnabled(target)); + assert.ok(testObject.isEnabled(dep)); + assert.strictEqual(testObject.getEnablementState(target), EnablementState.EnabledGlobally); + assert.strictEqual(testObject.getEnablementState(dep), EnablementState.EnabledGlobally); + }); + test('test remove an extension from disablement list when uninstalled', async () => { const extension = aLocalExtension('pub.a'); installed.push(extension); @@ -513,7 +538,7 @@ suite('ExtensionEnablementService Test', () => { const extension = aLocalExtension('pub.a'); installed.push(extension); - testObject.setEnablement([extension], EnablementState.EnabledWorkspace); + await testObject.setEnablement([extension], EnablementState.EnabledWorkspace); instantiationService.stub(IWorkbenchEnvironmentService, { enableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); @@ -525,7 +550,7 @@ suite('ExtensionEnablementService Test', () => { const extension = aLocalExtension('pub.a'); installed.push(extension); - testObject.setEnablement([extension], EnablementState.DisabledGlobally); + await testObject.setEnablement([extension], EnablementState.DisabledGlobally); instantiationService.stub(IWorkbenchEnvironmentService, { enableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); @@ -537,7 +562,7 @@ suite('ExtensionEnablementService Test', () => { const extension = aLocalExtension('pub.a'); installed.push(extension); - testObject.setEnablement([extension], EnablementState.DisabledWorkspace); + await testObject.setEnablement([extension], EnablementState.DisabledWorkspace); instantiationService.stub(IWorkbenchEnvironmentService, { enableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); -- cgit v1.2.3 From 1f2fdee4097dda63abed1ccbc0ef582955596b69 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 4 Apr 2022 14:17:31 +0200 Subject: files - expose watch options --- src/vs/platform/files/common/files.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/vs') diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 1b75291e899..028d33a315c 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -238,7 +238,7 @@ export interface IFileService { * Note: recursive file watching is not supported from this method. Only events from files * that are direct children of the provided resource will be reported. */ - watch(resource: URI): IDisposable; + watch(resource: URI, options?: IWatchOptions): IDisposable; /** * Frees up any resources occupied by this service. -- cgit v1.2.3 From a9c3c0e4b0384fe2b2c907b94a99fda18d3f34a6 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 4 Apr 2022 14:51:40 +0200 Subject: Fix file icon showing in custom tree views Fixes #146479 --- src/vs/workbench/browser/parts/views/treeView.ts | 29 +++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) (limited to 'src/vs') diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 91df2786f62..345a4f9e5a2 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -65,6 +65,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { Mimes } from 'vs/base/common/mime'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IDataTransfer } from 'vs/workbench/common/dnd'; +import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService'; export class TreeViewPane extends ViewPane { @@ -1018,13 +1019,14 @@ class TreeRenderer extends Disposable implements ITreeRenderer('explorer.decorations'); const labelResource = resource ? resource : URI.parse('missing:_icon_resource'); templateData.resourceLabel.setResource({ name: label, description, resource: labelResource }, { fileKind: this.getFileKind(node), title, - hideIcon: !!iconUrl || !!node.themeIcon, + hideIcon: !!iconUrl || this.shouldShowThemeIcon(!!resource, node.themeIcon), fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData), @@ -1047,7 +1049,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer Date: Mon, 4 Apr 2022 15:03:46 +0200 Subject: Fix typo --- .../workbench/contrib/comments/browser/commentThreadZoneWidget.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/vs') diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 151a49ffbb6..14ad6544915 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -85,7 +85,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private readonly _globalToDispose = new DisposableStore(); private _commentThreadDisposables: IDisposable[] = []; private _contextKeyService: IContextKeyService; - private _scopedInstatiationService: IInstantiationService; + private _scopedInstantiationService: IInstantiationService; public get owner(): string { return this._owner; @@ -109,7 +109,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget super(editor, { keepEditorSelection: true }); this._contextKeyService = contextKeyService.createScoped(this.domNode); - this._scopedInstatiationService = instantiationService.createChild(new ServiceCollection( + this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection( [IContextKeyService, this._contextKeyService] )); @@ -184,13 +184,13 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget protected _fillContainer(container: HTMLElement): void { this.setCssClass('review-widget'); - this._commentThreadWidget = this._scopedInstatiationService.createInstance( + this._commentThreadWidget = this._scopedInstantiationService.createInstance( CommentThreadWidget, container, this._owner, this.editor.getModel()!.uri, this._contextKeyService, - this._scopedInstatiationService, + this._scopedInstantiationService, this._commentThread as unknown as languages.CommentThread, this._pendingComment, { editor: this.editor }, -- cgit v1.2.3 From c27b6d1c243832bd6b6a433485ee43d23ba04d89 Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 4 Apr 2022 16:16:27 +0200 Subject: add `editor.inlayHints.toggle` option to quickly show or hide inlay hints, https://github.com/microsoft/vscode/issues/128162 --- src/vs/editor/common/config/editorOptions.ts | 18 +++++- .../inlayHints/browser/inlayHintsController.ts | 68 ++++++++++++++++------ src/vs/monaco.d.ts | 4 ++ 3 files changed, 72 insertions(+), 18 deletions(-) (limited to 'src/vs') diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 2d38b52d373..febb7dac4d6 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -2503,6 +2503,11 @@ export interface IEditorInlayHintsOptions { */ enabled?: boolean; + /** + * + */ + toggle?: 'show' | 'hide' | null; + /** * Font size of inline hints. * Default to 90% of the editor font size. @@ -2531,7 +2536,7 @@ export type EditorInlayHintsOptions = Readonly { constructor() { - const defaults: EditorInlayHintsOptions = { enabled: true, fontSize: 0, fontFamily: '', displayStyle: 'compact' }; + const defaults: EditorInlayHintsOptions = { enabled: true, toggle: null, fontSize: 0, fontFamily: '', displayStyle: 'compact' }; super( EditorOption.inlayHints, 'inlayHints', defaults, { @@ -2540,6 +2545,16 @@ class EditorInlayHints extends BaseEditorOption