diff options
author | Johannes Rieken <johannes.rieken@gmail.com> | 2022-04-04 11:13:25 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-04 11:13:25 +0300 |
commit | 4ef3ed3ce8d7ab1857d41454449d32f946d3ac8c (patch) | |
tree | f211e51646c00aa397afdb0f866b9a2aae4f17df /src | |
parent | d8eee16887698292885ea5f7bc4dac397d0377fb (diff) | |
parent | 2ada17080cd3e351e093e5fce40d36f16db3124a (diff) |
Merge branch 'main' into joh/cellUri
Diffstat (limited to 'src')
369 files changed, 7094 insertions, 5554 deletions
diff --git a/src/vs/base/browser/dnd.ts b/src/vs/base/browser/dnd.ts index 03143fb6361..e1a0872271a 100644 --- a/src/vs/base/browser/dnd.ts +++ b/src/vs/base/browser/dnd.ts @@ -71,12 +71,7 @@ export const DataTransfers = { /** * Typically transfer type for copy/paste transfers. */ - TEXT: Mimes.text, - - /** - * Application specific terminal transfer type. - */ - TERMINALS: 'Terminals' + TEXT: Mimes.text }; export function applyDragImage(event: DragEvent, label: string | null, clazz: string): void { diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 61108e58c67..f7b34497bd4 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -37,7 +37,7 @@ export interface MarkdownRenderOptions extends FormattedTextRenderOptions { /** * Low-level way create a html element from a markdown string. * - * **Note** that for most cases you should be using [`MarkdownRenderer`](./src/vs/editor/browser/core/markdownRenderer.ts) + * **Note** that for most cases you should be using [`MarkdownRenderer`](./src/vs/editor/contrib/markdownRenderer/browser/markdownRenderer.ts) * which comes with support for pretty code block rendering and which uses the default way of handling links. */ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: MarkedOptions = {}): { element: HTMLElement; dispose: () => void } { diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index 71ecf512f7c..7dbb5c1c71a 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/src/vs/base/browser/ui/actionbar/actionbar.css @@ -54,7 +54,7 @@ .monaco-action-bar .action-item.disabled .action-label, .monaco-action-bar .action-item.disabled .action-label::before, .monaco-action-bar .action-item.disabled .action-label:hover { - opacity: 0.4; + color: var(--vscode-disabledForeground); } /* Vertical actions */ diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 533ca04a116..e3528de5065 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -1072,7 +1072,7 @@ ${formatRule(Codicon.menuSubmenu)} .monaco-menu .monaco-action-bar .action-item.disabled .action-label, .monaco-menu .monaco-action-bar .action-item.disabled .action-label:hover { - opacity: 0.4; + color: var(--vscode-disabledForeground); } /* Vertical actions */ diff --git a/src/vs/base/browser/ui/splitview/paneview.css b/src/vs/base/browser/ui/splitview/paneview.css index 07981a17842..8610b867deb 100644 --- a/src/vs/base/browser/ui/splitview/paneview.css +++ b/src/vs/base/browser/ui/splitview/paneview.css @@ -77,8 +77,6 @@ min-width: 110px; min-height: 18px; padding: 2px 23px 2px 8px; - background-color: inherit !important; - color: inherit !important; } .linux .monaco-pane-view .pane > .pane-header .action-item .monaco-select-box, diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 97fe16f8a19..e9536b9b89b 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -435,6 +435,12 @@ export interface EmitterOptions { onLastListenerRemove?: Function; leakWarningThreshold?: number; + /** + * Pass in a delivery queue, which is useful for ensuring + * in order event delivery across multiple emitters. + */ + deliveryQueue?: EventDeliveryQueue; + /** ONLY enable this during development */ _profName?: string; } @@ -598,13 +604,14 @@ export class Emitter<T> { private readonly _perfMon?: EventProfiling; private _disposed: boolean = false; private _event?: Event<T>; - private _deliveryQueue?: LinkedList<[Listener<T>, T]>; + private _deliveryQueue?: EventDeliveryQueue; protected _listeners?: LinkedList<Listener<T>>; constructor(options?: EmitterOptions) { this._options = options; this._leakageMon = _globalLeakWarningThreshold > 0 ? new LeakageMonitor(this._options && this._options.leakWarningThreshold) : undefined; this._perfMon = this._options?._profName ? new EventProfiling(this._options._profName) : undefined; + this._deliveryQueue = this._options?.deliveryQueue; } dispose() { @@ -636,7 +643,7 @@ export class Emitter<T> { this._listeners.clear(); } - this._deliveryQueue?.clear(); + this._deliveryQueue?.clear(this); this._options?.onLastListenerRemove?.(); this._leakageMon?.dispose(); } @@ -720,24 +727,17 @@ export class Emitter<T> { // the driver of this if (!this._deliveryQueue) { - this._deliveryQueue = new LinkedList(); + this._deliveryQueue = new PrivateEventDeliveryQueue(); } for (let listener of this._listeners) { - this._deliveryQueue.push([listener, event]); + this._deliveryQueue.push(this, listener, event); } // start/stop performance insight collection this._perfMon?.start(this._deliveryQueue.size); - while (this._deliveryQueue.size > 0) { - const [listener, event] = this._deliveryQueue.shift()!; - try { - listener.invoke(event); - } catch (e) { - onUnexpectedError(e); - } - } + this._deliveryQueue.deliver(); this._perfMon?.stop(); } @@ -751,6 +751,57 @@ export class Emitter<T> { } } +export class EventDeliveryQueue { + protected _queue = new LinkedList<EventDeliveryQueueElement>(); + + get size(): number { + return this._queue.size; + } + + push<T>(emitter: Emitter<T>, listener: Listener<T>, event: T): void { + this._queue.push(new EventDeliveryQueueElement(emitter, listener, event)); + } + + clear<T>(emitter: Emitter<T>): void { + const newQueue = new LinkedList<EventDeliveryQueueElement>(); + for (const element of this._queue) { + if (element.emitter !== emitter) { + newQueue.push(element); + } + } + this._queue = newQueue; + } + + deliver(): void { + while (this._queue.size > 0) { + const element = this._queue.shift()!; + try { + element.listener.invoke(element.event); + } catch (e) { + onUnexpectedError(e); + } + } + } +} + +/** + * An `EventDeliveryQueue` that is guaranteed to be used by a single `Emitter`. + */ +class PrivateEventDeliveryQueue extends EventDeliveryQueue { + override clear<T>(emitter: Emitter<T>): void { + // Here we can just clear the entire linked list because + // all elements are guaranteed to belong to this emitter + this._queue.clear(); + } +} + +class EventDeliveryQueueElement<T = any> { + constructor( + readonly emitter: Emitter<T>, + readonly listener: Listener<T>, + readonly event: T + ) { } +} export interface IWaitUntil { token: CancellationToken; diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts index fd691e9f568..3eb569d70e7 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -357,7 +357,7 @@ function wrapRelativePattern(parsedPattern: ParsedStringPattern, arg2: string | return parsedPattern; } - return function (path, basename) { + const wrappedPattern: ParsedStringPattern = function (path, basename) { if (!isEqualOrParent(path, arg2.base, !isLinux)) { // skip glob matching if `base` is not a parent of `path` return null; @@ -368,6 +368,14 @@ function wrapRelativePattern(parsedPattern: ParsedStringPattern, arg2: string | // and only match on the remaining path components return parsedPattern(path.substr(arg2.base.length + 1), basename); }; + + // Make sure to preserve associated metadata + wrappedPattern.allBasenames = parsedPattern.allBasenames; + wrappedPattern.allPaths = parsedPattern.allPaths; + wrappedPattern.basenames = parsedPattern.basenames; + wrappedPattern.patterns = parsedPattern.patterns; + + return wrappedPattern; } function trimForExclusions(pattern: string, options: IGlobOptions): string { diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 97370fd6f2c..c7d1b418241 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -845,6 +845,67 @@ export class ResourceMap<T> implements Map<URI, T> { } } +export class ResourceSet implements Set<URI> { + + readonly [Symbol.toStringTag]: string = 'ResourceSet'; + + private readonly _map: ResourceMap<URI>; + + constructor(toKey?: ResourceMapKeyFn); + constructor(entries: readonly URI[], toKey?: ResourceMapKeyFn); + constructor(entriesOrKey?: readonly URI[] | ResourceMapKeyFn, toKey?: ResourceMapKeyFn) { + if (!entriesOrKey || typeof entriesOrKey === 'function') { + this._map = new ResourceMap(entriesOrKey); + } else { + this._map = new ResourceMap(toKey); + entriesOrKey.forEach(this.add, this); + } + } + + + get size(): number { + return this._map.size; + } + + add(value: URI): this { + this._map.set(value, value); + return this; + } + + clear(): void { + this._map.clear(); + } + + delete(value: URI): boolean { + return this._map.delete(value); + } + + forEach(callbackfn: (value: URI, value2: URI, set: Set<URI>) => void, thisArg?: any): void { + this._map.forEach((_value, key) => callbackfn.call(thisArg, key, key, this)); + } + + has(value: URI): boolean { + return this._map.has(value); + } + + entries(): IterableIterator<[URI, URI]> { + return this._map.entries(); + } + + keys(): IterableIterator<URI> { + return this._map.keys(); + } + + values(): IterableIterator<URI> { + return this._map.keys(); + } + + [Symbol.iterator](): IterableIterator<URI> { + return this.keys(); + } +} + + interface Item<K, V> { previous: Item<K, V> | undefined; next: Item<K, V> | undefined; diff --git a/src/vs/base/node/ps.ts b/src/vs/base/node/ps.ts index 0a4d82d0ae4..4eded868127 100644 --- a/src/vs/base/node/ps.ts +++ b/src/vs/base/node/ps.ts @@ -133,6 +133,10 @@ export function listProcesses(rootPid: number): Promise<ProcessItem> { (import('windows-process-tree')).then(windowsProcessTree => { windowsProcessTree.getProcessList(rootPid, (processList) => { + if (!processList) { + reject(new Error(`Root process ${rootPid} not found`)); + return; + } windowsProcessTree.getProcessCpuUsage(processList, (completeProcessList) => { const processItems: Map<number, ProcessItem> = new Map(); completeProcessList.forEach(process => { diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 51bc034c083..7f607bd1360 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -1109,7 +1109,7 @@ export namespace ProxyChannel { properties?: Map<string, unknown>; } - export function toService<T>(channel: IChannel, options?: ICreateProxyServiceOptions): T { + export function toService<T extends object>(channel: IChannel, options?: ICreateProxyServiceOptions): T { const disableMarshalling = options && options.disableMarshalling; return new Proxy({}, { diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 9eb6fa7e574..ee3543a2203 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -1249,6 +1249,7 @@ export class QuickInputController extends Disposable { const inputBox = this._register(new QuickInputBox(filterContainer)); inputBox.setAttribute('aria-describedby', `${this.idPrefix}message`); + inputBox.setAttribute('aria-live', 'polite'); const visibleCountContainer = dom.append(filterContainer, $('.quick-input-visible-count')); visibleCountContainer.setAttribute('aria-live', 'polite'); diff --git a/src/vs/base/parts/storage/node/storage.ts b/src/vs/base/parts/storage/node/storage.ts index 66fd5e19a47..e8bfec83a24 100644 --- a/src/vs/base/parts/storage/node/storage.ts +++ b/src/vs/base/parts/storage/node/storage.ts @@ -223,13 +223,6 @@ export class SQLiteStorageDatabase implements IStorageDatabase { return `${path}.backup`; } - async vacuum(): Promise<void> { - this.logger.trace(`[storage ${this.name}] vacuum()`); - - const connection = await this.whenConnected; - await this.exec(connection, 'VACUUM'); - } - async checkIntegrity(full: boolean): Promise<string> { this.logger.trace(`[storage ${this.name}] checkIntegrity(full: ${full})`); diff --git a/src/vs/base/parts/storage/test/node/storage.test.ts b/src/vs/base/parts/storage/test/node/storage.test.ts index c686419c338..ba47114a16b 100644 --- a/src/vs/base/parts/storage/test/node/storage.test.ts +++ b/src/vs/base/parts/storage/test/node/storage.test.ts @@ -623,8 +623,7 @@ flakySuite('SQLite Storage Library', function () { }); test('very large item value', async function () { - const dbPath = join(testdir, 'storage.db'); - let storage = new SQLiteStorageDatabase(dbPath); + let storage = new SQLiteStorageDatabase(join(testdir, 'storage.db')); const items = new Map<string, string>(); items.set('colorthemedata', '{"id":"vs vscode-theme-defaults-themes-light_plus-json","label":"Light+ (default light)","settingsId":"Default Light+","selector":"vs.vscode-theme-defaults-themes-light_plus-json","themeTokenColors":[{"settings":{"foreground":"#000000ff","background":"#ffffffff"}},{"scope":["meta.embedded","source.groovy.embedded"],"settings":{"foreground":"#000000ff"}},{"scope":"emphasis","settings":{"fontStyle":"italic"}},{"scope":"strong","settings":{"fontStyle":"bold"}},{"scope":"meta.diff.header","settings":{"foreground":"#000080"}},{"scope":"comment","settings":{"foreground":"#008000"}},{"scope":"constant.language","settings":{"foreground":"#0000ff"}},{"scope":["constant.numeric"],"settings":{"foreground":"#098658"}},{"scope":"constant.regexp","settings":{"foreground":"#811f3f"}},{"name":"css tags in selectors, xml tags","scope":"entity.name.tag","settings":{"foreground":"#800000"}},{"scope":"entity.name.selector","settings":{"foreground":"#800000"}},{"scope":"entity.other.attribute-name","settings":{"foreground":"#ff0000"}},{"scope":["entity.other.attribute-name.class.css","entity.other.attribute-name.class.mixin.css","entity.other.attribute-name.id.css","entity.other.attribute-name.parent-selector.css","entity.other.attribute-name.pseudo-class.css","entity.other.attribute-name.pseudo-element.css","source.css.less entity.other.attribute-name.id","entity.other.attribute-name.attribute.scss","entity.other.attribute-name.scss"],"settings":{"foreground":"#800000"}},{"scope":"invalid","settings":{"foreground":"#cd3131"}},{"scope":"markup.underline","settings":{"fontStyle":"underline"}},{"scope":"markup.bold","settings":{"fontStyle":"bold","foreground":"#000080"}},{"scope":"markup.heading","settings":{"fontStyle":"bold","foreground":"#800000"}},{"scope":"markup.italic","settings":{"fontStyle":"italic"}},{"scope":"markup.inserted","settings":{"foreground":"#098658"}},{"scope":"markup.deleted","settings":{"foreground":"#a31515"}},{"scope":"markup.changed","settings":{"foreground":"#0451a5"}},{"scope":["punctuation.definition.quote.begin.markdown","punctuation.definition.list.begin.markdown"],"settings":{"foreground":"#0451a5"}},{"scope":"markup.inline.raw","settings":{"foreground":"#800000"}},{"name":"brackets of XML/HTML tags","scope":"punctuation.definition.tag","settings":{"foreground":"#800000"}},{"scope":"meta.preprocessor","settings":{"foreground":"#0000ff"}},{"scope":"meta.preprocessor.string","settings":{"foreground":"#a31515"}},{"scope":"meta.preprocessor.numeric","settings":{"foreground":"#098658"}},{"scope":"meta.structure.dictionary.key.python","settings":{"foreground":"#0451a5"}},{"scope":"storage","settings":{"foreground":"#0000ff"}},{"scope":"storage.type","settings":{"foreground":"#0000ff"}},{"scope":"storage.modifier","settings":{"foreground":"#0000ff"}},{"scope":"string","settings":{"foreground":"#a31515"}},{"scope":["string.comment.buffered.block.pug","string.quoted.pug","string.interpolated.pug","string.unquoted.plain.in.yaml","string.unquoted.plain.out.yaml","string.unquoted.block.yaml","string.quoted.single.yaml","string.quoted.double.xml","string.quoted.single.xml","string.unquoted.cdata.xml","string.quoted.double.html","string.quoted.single.html","string.unquoted.html","string.quoted.single.handlebars","string.quoted.double.handlebars"],"settings":{"foreground":"#0000ff"}},{"scope":"string.regexp","settings":{"foreground":"#811f3f"}},{"name":"String interpolation","scope":["punctuation.definition.template-expression.begin","punctuation.definition.template-expression.end","punctuation.section.embedded"],"settings":{"foreground":"#0000ff"}},{"name":"Reset JavaScript string interpolation expression","scope":["meta.template.expression"],"settings":{"foreground":"#000000"}},{"scope":["support.constant.property-value","support.constant.font-name","support.constant.media-type","support.constant.media","constant.other.color.rgb-value","constant.other.rgb-value","support.constant.color"],"settings":{"foreground":"#0451a5"}},{"scope":["support.type.vendored.property-name","support.type.property-name","variable.css","variable.scss","variable.other.less","source.coffee.embedded"],"settings":{"foreground":"#ff0000"}},{"scope":["support.type.property-name.json"],"settings":{"foreground":"#0451a5"}},{"scope":"keyword","settings":{"foreground":"#0000ff"}},{"scope":"keyword.control","settings":{"foreground":"#0000ff"}},{"scope":"keyword.operator","settings":{"foreground":"#000000"}},{"scope":["keyword.operator.new","keyword.operator.expression","keyword.operator.cast","keyword.operator.sizeof","keyword.operator.instanceof","keyword.operator.logical.python"],"settings":{"foreground":"#0000ff"}},{"scope":"keyword.other.unit","settings":{"foreground":"#098658"}},{"scope":["punctuation.section.embedded.begin.php","punctuation.section.embedded.end.php"],"settings":{"foreground":"#800000"}},{"scope":"support.function.git-rebase","settings":{"foreground":"#0451a5"}},{"scope":"constant.sha.git-rebase","settings":{"foreground":"#098658"}},{"name":"coloring of the Java import and package identifiers","scope":["storage.modifier.import.java","variable.language.wildcard.java","storage.modifier.package.java"],"settings":{"foreground":"#000000"}},{"name":"this.self","scope":"variable.language","settings":{"foreground":"#0000ff"}},{"name":"Function declarations","scope":["entity.name.function","support.function","support.constant.handlebars"],"settings":{"foreground":"#795E26"}},{"name":"Types declaration and references","scope":["meta.return-type","support.class","support.type","entity.name.type","entity.name.class","storage.type.numeric.go","storage.type.byte.go","storage.type.boolean.go","storage.type.string.go","storage.type.uintptr.go","storage.type.error.go","storage.type.rune.go","storage.type.cs","storage.type.generic.cs","storage.type.modifier.cs","storage.type.variable.cs","storage.type.annotation.java","storage.type.generic.java","storage.type.java","storage.type.object.array.java","storage.type.primitive.array.java","storage.type.primitive.java","storage.type.token.java","storage.type.groovy","storage.type.annotation.groovy","storage.type.parameters.groovy","storage.type.generic.groovy","storage.type.object.array.groovy","storage.type.primitive.array.groovy","storage.type.primitive.groovy"],"settings":{"foreground":"#267f99"}},{"name":"Types declaration and references, TS grammar specific","scope":["meta.type.cast.expr","meta.type.new.expr","support.constant.math","support.constant.dom","support.constant.json","entity.other.inherited-class"],"settings":{"foreground":"#267f99"}},{"name":"Control flow keywords","scope":"keyword.control","settings":{"foreground":"#AF00DB"}},{"name":"Variable and parameter name","scope":["variable","meta.definition.variable.name","support.variable","entity.name.variable"],"settings":{"foreground":"#001080"}},{"name":"Object keys, TS grammar specific","scope":["meta.object-literal.key"],"settings":{"foreground":"#001080"}},{"name":"CSS property value","scope":["support.constant.property-value","support.constant.font-name","support.constant.media-type","support.constant.media","constant.other.color.rgb-value","constant.other.rgb-value","support.constant.color"],"settings":{"foreground":"#0451a5"}},{"name":"Regular expression groups","scope":["punctuation.definition.group.regexp","punctuation.definition.group.assertion.regexp","punctuation.definition.character-class.regexp","punctuation.character.set.begin.regexp","punctuation.character.set.end.regexp","keyword.operator.negation.regexp","support.other.parenthesis.regexp"],"settings":{"foreground":"#d16969"}},{"scope":["constant.character.character-class.regexp","constant.other.character-class.set.regexp","constant.other.character-class.regexp","constant.character.set.regexp"],"settings":{"foreground":"#811f3f"}},{"scope":"keyword.operator.quantifier.regexp","settings":{"foreground":"#000000"}},{"scope":["keyword.operator.or.regexp","keyword.control.anchor.regexp"],"settings":{"foreground":"#ff0000"}},{"scope":"constant.character","settings":{"foreground":"#0000ff"}},{"scope":"constant.character.escape","settings":{"foreground":"#ff0000"}},{"scope":"token.info-token","settings":{"foreground":"#316bcd"}},{"scope":"token.warn-token","settings":{"foreground":"#cd9731"}},{"scope":"token.error-token","settings":{"foreground":"#cd3131"}},{"scope":"token.debug-token","settings":{"foreground":"#800080"}}],"extensionData":{"extensionId":"vscode.theme-defaults","extensionPublisher":"vscode","extensionName":"theme-defaults","extensionIsBuiltin":true},"colorMap":{"editor.background":"#ffffff","editor.foreground":"#000000","editor.inactiveSelectionBackground":"#e5ebf1","editorIndentGuide.background":"#d3d3d3","editorIndentGuide.activeBackground":"#939393","editor.selectionHighlightBackground":"#add6ff4d","editorSuggestWidget.background":"#f3f3f3","activityBarBadge.background":"#007acc","sideBarTitle.foreground":"#6f6f6f","list.hoverBackground":"#e8e8e8","input.placeholderForeground":"#767676","settings.textInputBorder":"#cecece","settings.numberInputBorder":"#cecece"}}'); @@ -668,18 +667,6 @@ flakySuite('SQLite Storage Library', function () { ok(!storedItems.get('super.large.string')); await storage.close(); - - // Vacuum - - storage = new SQLiteStorageDatabase(dbPath); - - const sizeBeforeVacuum = (await Promises.stat(dbPath)).size; - await storage.vacuum(); - const sizeAfterVacuum = (await Promises.stat(dbPath)).size; - - ok(sizeBeforeVacuum > sizeAfterVacuum); - - await storage.close(); }); test('multiple concurrent writes execute in sequence', async () => { diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 6821fcada34..612f54c17a1 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -341,8 +341,7 @@ class WorkspaceProvider implements IWorkspaceProvider { }); // If no workspace is provided through the URL, check for config - // attribute from server and fallback to last opened workspace - // from storage + // attribute from server if (!foundWorkspace) { if (config.folderUri) { workspace = { folderUri: URI.revive(config.folderUri) }; diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts index f64b77fc749..f26355d5692 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts @@ -5,10 +5,11 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; +import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { migrateUnsupportedExtensions } from 'vs/platform/extensionManagement/common/unsupportedExtensionsMigration'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ILogService } from 'vs/platform/log/common/log'; +import { IStorageService } from 'vs/platform/storage/common/storage'; export class ExtensionsCleaner extends Disposable { @@ -17,10 +18,12 @@ export class ExtensionsCleaner extends Disposable { @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @IExtensionStorageService extensionStorageService: IExtensionStorageService, @IGlobalExtensionEnablementService extensionEnablementService: IGlobalExtensionEnablementService, + @IStorageService storageService: IStorageService, @ILogService logService: ILogService, ) { super(); extensionManagementService.removeDeprecatedExtensions(); migrateUnsupportedExtensions(extensionManagementService, extensionGalleryService, extensionStorageService, extensionEnablementService, logService); + ExtensionStorageService.removeOutdatedExtensionVersions(extensionManagementService, storageService); } } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index d7459f90ea6..5d6b367dca0 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -524,7 +524,7 @@ export class CodeApplication extends Disposable { // Create driver if (this.environmentMainService.driverHandle) { - const server = await serveDriver(mainProcessElectronServer, this.environmentMainService.driverHandle, this.environmentMainService, appInstantiationService); + const server = await serveDriver(mainProcessElectronServer, this.environmentMainService.driverHandle, appInstantiationService); this.logService.info('Driver started at:', this.environmentMainService.driverHandle); this._register(server); diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index f8b64a64f0f..10e33dbfa7c 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -165,7 +165,7 @@ class CliMain extends Disposable { commonProperties: (async () => { let machineId: string | undefined = undefined; try { - const storageContents = await Promises.readFile(joinPath(environmentService.appSettingsHome, 'storage.json').fsPath); + const storageContents = await Promises.readFile(joinPath(environmentService.globalStorageHome, 'storage.json').fsPath); machineId = JSON.parse(storageContents.toString())[machineIdKey]; } catch (error) { if (error.code !== 'ENOENT') { diff --git a/src/vs/editor/browser/config/editorConfiguration.ts b/src/vs/editor/browser/config/editorConfiguration.ts index 89f191b75ed..58474ac9363 100644 --- a/src/vs/editor/browser/config/editorConfiguration.ts +++ b/src/vs/editor/browser/config/editorConfiguration.ts @@ -291,7 +291,7 @@ class EditorOptionsUtil { if (Array.isArray(a) || Array.isArray(b)) { return (Array.isArray(a) && Array.isArray(b) ? arrays.equals(a, b) : false); } - if (Object.keys(a).length !== Object.keys(b).length) { + if (Object.keys(a as unknown as object).length !== Object.keys(b as unknown as object).length) { return false; } for (const key in a) { diff --git a/src/vs/editor/browser/config/migrateOptions.ts b/src/vs/editor/browser/config/migrateOptions.ts index 713fd69f4bd..f5af4fdb563 100644 --- a/src/vs/editor/browser/config/migrateOptions.ts +++ b/src/vs/editor/browser/config/migrateOptions.ts @@ -6,150 +6,160 @@ import { forEach } from 'vs/base/common/collections'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -/** - * Compatibility with old options - */ -export function migrateOptions(options: IEditorOptions): void { - const wordWrap = options.wordWrap; - if (<any>wordWrap === true) { - options.wordWrap = 'on'; - } else if (<any>wordWrap === false) { - options.wordWrap = 'off'; - } - - const lineNumbers = options.lineNumbers; - if (<any>lineNumbers === true) { - options.lineNumbers = 'on'; - } else if (<any>lineNumbers === false) { - options.lineNumbers = 'off'; - } - - const autoClosingBrackets = options.autoClosingBrackets; - if (<any>autoClosingBrackets === false) { - options.autoClosingBrackets = 'never'; - options.autoClosingQuotes = 'never'; - options.autoSurround = 'never'; - } +export interface ISettingsReader { + (key: string): any; +} - const cursorBlinking = options.cursorBlinking; - if (<any>cursorBlinking === 'visible') { - options.cursorBlinking = 'solid'; - } +export interface ISettingsWriter { + (key: string, value: any): void; +} - const renderWhitespace = options.renderWhitespace; - if (<any>renderWhitespace === true) { - options.renderWhitespace = 'boundary'; - } else if (<any>renderWhitespace === false) { - options.renderWhitespace = 'none'; - } +export class EditorSettingMigration { - const renderLineHighlight = options.renderLineHighlight; - if (<any>renderLineHighlight === true) { - options.renderLineHighlight = 'line'; - } else if (<any>renderLineHighlight === false) { - options.renderLineHighlight = 'none'; - } + public static items: EditorSettingMigration[] = []; - const acceptSuggestionOnEnter = options.acceptSuggestionOnEnter; - if (<any>acceptSuggestionOnEnter === true) { - options.acceptSuggestionOnEnter = 'on'; - } else if (<any>acceptSuggestionOnEnter === false) { - options.acceptSuggestionOnEnter = 'off'; - } + constructor( + public readonly key: string, + public readonly migrate: (value: any, read: ISettingsReader, write: ISettingsWriter) => void + ) { } - const tabCompletion = options.tabCompletion; - if (<any>tabCompletion === false) { - options.tabCompletion = 'off'; - } else if (<any>tabCompletion === true) { - options.tabCompletion = 'onlySnippets'; + apply(options: any): void { + const value = EditorSettingMigration._read(options, this.key); + const read = (key: string) => EditorSettingMigration._read(options, key); + const write = (key: string, value: any) => EditorSettingMigration._write(options, key, value); + this.migrate(value, read, write); } - const suggest = options.suggest; - if (suggest && typeof (<any>suggest).filteredTypes === 'object' && (<any>suggest).filteredTypes) { - const mapping: Record<string, string> = {}; - mapping['method'] = 'showMethods'; - mapping['function'] = 'showFunctions'; - mapping['constructor'] = 'showConstructors'; - mapping['deprecated'] = 'showDeprecated'; - mapping['field'] = 'showFields'; - mapping['variable'] = 'showVariables'; - mapping['class'] = 'showClasses'; - mapping['struct'] = 'showStructs'; - mapping['interface'] = 'showInterfaces'; - mapping['module'] = 'showModules'; - mapping['property'] = 'showProperties'; - mapping['event'] = 'showEvents'; - mapping['operator'] = 'showOperators'; - mapping['unit'] = 'showUnits'; - mapping['value'] = 'showValues'; - mapping['constant'] = 'showConstants'; - mapping['enum'] = 'showEnums'; - mapping['enumMember'] = 'showEnumMembers'; - mapping['keyword'] = 'showKeywords'; - mapping['text'] = 'showWords'; - mapping['color'] = 'showColors'; - mapping['file'] = 'showFiles'; - mapping['reference'] = 'showReferences'; - mapping['folder'] = 'showFolders'; - mapping['typeParameter'] = 'showTypeParameters'; - mapping['snippet'] = 'showSnippets'; - forEach(mapping, entry => { - const value = (<any>suggest).filteredTypes[entry.key]; - if (value === false) { - (<any>suggest)[entry.value] = value; - } - }); - // delete (<any>suggest).filteredTypes; - } + private static _read(source: any, key: string): any { + if (typeof source === 'undefined') { + return undefined; + } - const hover = options.hover; - if (<any>hover === true) { - options.hover = { - enabled: true - }; - } else if (<any>hover === false) { - options.hover = { - enabled: false - }; + const firstDotIndex = key.indexOf('.'); + if (firstDotIndex >= 0) { + const firstSegment = key.substring(0, firstDotIndex); + return this._read(source[firstSegment], key.substring(firstDotIndex + 1)); + } + return source[key]; } - const parameterHints = options.parameterHints; - if (<any>parameterHints === true) { - options.parameterHints = { - enabled: true - }; - } else if (<any>parameterHints === false) { - options.parameterHints = { - enabled: false - }; + private static _write(target: any, key: string, value: any): void { + const firstDotIndex = key.indexOf('.'); + if (firstDotIndex >= 0) { + const firstSegment = key.substring(0, firstDotIndex); + target[firstSegment] = target[firstSegment] || {}; + this._write(target[firstSegment], key.substring(firstDotIndex + 1), value); + return; + } + target[key] = value; } +} - const autoIndent = options.autoIndent; - if (<any>autoIndent === true) { - options.autoIndent = 'full'; - } else if (<any>autoIndent === false) { - options.autoIndent = 'advanced'; - } +function registerEditorSettingMigration(key: string, migrate: (value: any, read: ISettingsReader, write: ISettingsWriter) => void): void { + EditorSettingMigration.items.push(new EditorSettingMigration(key, migrate)); +} - const matchBrackets = options.matchBrackets; - if (<any>matchBrackets === true) { - options.matchBrackets = 'always'; - } else if (<any>matchBrackets === false) { - options.matchBrackets = 'never'; - } +function registerSimpleEditorSettingMigration(key: string, values: [any, any][]): void { + registerEditorSettingMigration(key, (value, read, write) => { + if (typeof value !== 'undefined') { + for (const [oldValue, newValue] of values) { + if (value === oldValue) { + write(key, newValue); + return; + } + } + } + }); +} - const { renderIndentGuides, highlightActiveIndentGuide } = options as any as { - renderIndentGuides: boolean; - highlightActiveIndentGuide: boolean; - }; - if (!options.guides) { - options.guides = {}; - } +/** + * Compatibility with old options + */ +export function migrateOptions(options: IEditorOptions): void { + EditorSettingMigration.items.forEach(migration => migration.apply(options)); +} - if (renderIndentGuides !== undefined) { - options.guides.indentation = !!renderIndentGuides; - } - if (highlightActiveIndentGuide !== undefined) { - options.guides.highlightActiveIndentation = !!highlightActiveIndentGuide; +registerSimpleEditorSettingMigration('wordWrap', [[true, 'on'], [false, 'off']]); +registerSimpleEditorSettingMigration('lineNumbers', [[true, 'on'], [false, 'off']]); +registerSimpleEditorSettingMigration('cursorBlinking', [['visible', 'solid']]); +registerSimpleEditorSettingMigration('renderWhitespace', [[true, 'boundary'], [false, 'none']]); +registerSimpleEditorSettingMigration('renderLineHighlight', [[true, 'line'], [false, 'none']]); +registerSimpleEditorSettingMigration('acceptSuggestionOnEnter', [[true, 'on'], [false, 'off']]); +registerSimpleEditorSettingMigration('tabCompletion', [[false, 'off'], [true, 'onlySnippets']]); +registerSimpleEditorSettingMigration('hover', [[true, { enabled: true }], [false, { enabled: false }]]); +registerSimpleEditorSettingMigration('parameterHints', [[true, { enabled: true }], [false, { enabled: false }]]); +registerSimpleEditorSettingMigration('autoIndent', [[false, 'advanced'], [true, 'full']]); +registerSimpleEditorSettingMigration('matchBrackets', [[true, 'always'], [false, 'never']]); + +registerEditorSettingMigration('autoClosingBrackets', (value, read, write) => { + if (value === false) { + write('autoClosingBrackets', 'never'); + if (typeof read('autoClosingQuotes') === 'undefined') { + write('autoClosingQuotes', 'never'); + } + if (typeof read('autoSurround') === 'undefined') { + write('autoSurround', 'never'); + } + } +}); + +registerEditorSettingMigration('renderIndentGuides', (value, read, write) => { + if (typeof value !== 'undefined') { + write('renderIndentGuides', undefined); + if (typeof read('guides.indentation') === 'undefined') { + write('guides.indentation', !!value); + } + } +}); + +registerEditorSettingMigration('highlightActiveIndentGuide', (value, read, write) => { + if (typeof value !== 'undefined') { + write('highlightActiveIndentGuide', undefined); + if (typeof read('guides.highlightActiveIndentation') === 'undefined') { + write('guides.highlightActiveIndentation', !!value); + } + } +}); + +const suggestFilteredTypesMapping: Record<string, string> = { + method: 'showMethods', + function: 'showFunctions', + constructor: 'showConstructors', + deprecated: 'showDeprecated', + field: 'showFields', + variable: 'showVariables', + class: 'showClasses', + struct: 'showStructs', + interface: 'showInterfaces', + module: 'showModules', + property: 'showProperties', + event: 'showEvents', + operator: 'showOperators', + unit: 'showUnits', + value: 'showValues', + constant: 'showConstants', + enum: 'showEnums', + enumMember: 'showEnumMembers', + keyword: 'showKeywords', + text: 'showWords', + color: 'showColors', + file: 'showFiles', + reference: 'showReferences', + folder: 'showFolders', + typeParameter: 'showTypeParameters', + snippet: 'showSnippets', +}; + +registerEditorSettingMigration('suggest.filteredTypes', (value, read, write) => { + if (value && typeof value === 'object') { + forEach(suggestFilteredTypesMapping, entry => { + const v = value[entry.key]; + if (v === false) { + if (typeof read(`suggest.${entry.value}`) === 'undefined') { + write(`suggest.${entry.value}`, false); + } + } + }); + write('suggest.filteredTypes', undefined); } -} +}); diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index 251a958e602..558376fae33 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -75,6 +75,7 @@ export interface IBulkEditOptions { undoRedoSource?: UndoRedoSource; undoRedoGroupId?: number; confirmBeforeUndo?: boolean; + respectAutoSaveConfig?: boolean; } export interface IBulkEditResult { diff --git a/src/vs/editor/browser/services/webWorker.ts b/src/vs/editor/browser/services/webWorker.ts index a88c165a758..c853e3220ec 100644 --- a/src/vs/editor/browser/services/webWorker.ts +++ b/src/vs/editor/browser/services/webWorker.ts @@ -13,7 +13,7 @@ import { ILanguageConfigurationService } from 'vs/editor/common/languages/langua * Create a new web worker that has model syncing capabilities built in. * Specify an AMD module to load that will `create` an object that will be proxied. */ -export function createWebWorker<T>(modelService: IModelService, languageConfigurationService: ILanguageConfigurationService, opts: IWebWorkerOptions): MonacoWebWorker<T> { +export function createWebWorker<T extends object>(modelService: IModelService, languageConfigurationService: ILanguageConfigurationService, opts: IWebWorkerOptions): MonacoWebWorker<T> { return new MonacoWebWorkerImpl<T>(modelService, languageConfigurationService, opts); } @@ -61,7 +61,7 @@ export interface IWebWorkerOptions { keepIdleModels?: boolean; } -class MonacoWebWorkerImpl<T> extends EditorWorkerClient implements MonacoWebWorker<T> { +class MonacoWebWorkerImpl<T extends object> extends EditorWorkerClient implements MonacoWebWorker<T> { private readonly _foreignModuleId: string; private readonly _foreignModuleHost: { [method: string]: Function } | null; diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index 1cc1d74fc1b..489de25caf0 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -131,7 +131,13 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { let result = ''; const leftOffset = ctx.visibleRangeForPosition(new Position(lineNumber, 1))?.left ?? 0; for (const guide of indent) { - const left = leftOffset + (guide.visibleColumn - 1) * this._spaceWidth; + const left = + guide.column === -1 + ? leftOffset + (guide.visibleColumn - 1) * this._spaceWidth + : ctx.visibleRangeForPosition( + new Position(lineNumber, guide.column) + )!.left; + if (left > scrollWidth || (this._maxIndentLeft > 0 && left > this._maxIndentLeft)) { break; } @@ -217,8 +223,11 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { lineGuides.push( new IndentGuide( indentGuide, + -1, isActive ? 'core-guide-indent-active' : 'core-guide-indent', - null + null, + -1, + -1, ) ); } diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index e6f64a9ed2a..2dcbf601e37 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -12,7 +12,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { Color } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter, EmitterOptions, Event, EventDeliveryQueue } from 'vs/base/common/event'; import { hash } from 'vs/base/common/hash'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; @@ -116,114 +116,117 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE }); //#region Eventing + + private readonly _deliveryQueue = new EventDeliveryQueue(); + private readonly _onDidDispose: Emitter<void> = this._register(new Emitter<void>()); public readonly onDidDispose: Event<void> = this._onDidDispose.event; - private readonly _onDidChangeModelContent: Emitter<IModelContentChangedEvent> = this._register(new Emitter<IModelContentChangedEvent>()); + private readonly _onDidChangeModelContent: Emitter<IModelContentChangedEvent> = this._register(new Emitter<IModelContentChangedEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeModelContent: Event<IModelContentChangedEvent> = this._onDidChangeModelContent.event; - private readonly _onDidChangeModelLanguage: Emitter<IModelLanguageChangedEvent> = this._register(new Emitter<IModelLanguageChangedEvent>()); + private readonly _onDidChangeModelLanguage: Emitter<IModelLanguageChangedEvent> = this._register(new Emitter<IModelLanguageChangedEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeModelLanguage: Event<IModelLanguageChangedEvent> = this._onDidChangeModelLanguage.event; - private readonly _onDidChangeModelLanguageConfiguration: Emitter<IModelLanguageConfigurationChangedEvent> = this._register(new Emitter<IModelLanguageConfigurationChangedEvent>()); + private readonly _onDidChangeModelLanguageConfiguration: Emitter<IModelLanguageConfigurationChangedEvent> = this._register(new Emitter<IModelLanguageConfigurationChangedEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeModelLanguageConfiguration: Event<IModelLanguageConfigurationChangedEvent> = this._onDidChangeModelLanguageConfiguration.event; - private readonly _onDidChangeModelOptions: Emitter<IModelOptionsChangedEvent> = this._register(new Emitter<IModelOptionsChangedEvent>()); + private readonly _onDidChangeModelOptions: Emitter<IModelOptionsChangedEvent> = this._register(new Emitter<IModelOptionsChangedEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeModelOptions: Event<IModelOptionsChangedEvent> = this._onDidChangeModelOptions.event; - private readonly _onDidChangeModelDecorations: Emitter<IModelDecorationsChangedEvent> = this._register(new Emitter<IModelDecorationsChangedEvent>()); + private readonly _onDidChangeModelDecorations: Emitter<IModelDecorationsChangedEvent> = this._register(new Emitter<IModelDecorationsChangedEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeModelDecorations: Event<IModelDecorationsChangedEvent> = this._onDidChangeModelDecorations.event; - private readonly _onDidChangeModelTokens: Emitter<IModelTokensChangedEvent> = this._register(new Emitter<IModelTokensChangedEvent>()); + private readonly _onDidChangeModelTokens: Emitter<IModelTokensChangedEvent> = this._register(new Emitter<IModelTokensChangedEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeModelTokens: Event<IModelTokensChangedEvent> = this._onDidChangeModelTokens.event; - private readonly _onDidChangeConfiguration: Emitter<ConfigurationChangedEvent> = this._register(new Emitter<ConfigurationChangedEvent>()); + private readonly _onDidChangeConfiguration: Emitter<ConfigurationChangedEvent> = this._register(new Emitter<ConfigurationChangedEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeConfiguration: Event<ConfigurationChangedEvent> = this._onDidChangeConfiguration.event; - protected readonly _onDidChangeModel: Emitter<editorCommon.IModelChangedEvent> = this._register(new Emitter<editorCommon.IModelChangedEvent>()); + protected readonly _onDidChangeModel: Emitter<editorCommon.IModelChangedEvent> = this._register(new Emitter<editorCommon.IModelChangedEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeModel: Event<editorCommon.IModelChangedEvent> = this._onDidChangeModel.event; - private readonly _onDidChangeCursorPosition: Emitter<ICursorPositionChangedEvent> = this._register(new Emitter<ICursorPositionChangedEvent>()); + private readonly _onDidChangeCursorPosition: Emitter<ICursorPositionChangedEvent> = this._register(new Emitter<ICursorPositionChangedEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeCursorPosition: Event<ICursorPositionChangedEvent> = this._onDidChangeCursorPosition.event; - private readonly _onDidChangeCursorSelection: Emitter<ICursorSelectionChangedEvent> = this._register(new Emitter<ICursorSelectionChangedEvent>()); + private readonly _onDidChangeCursorSelection: Emitter<ICursorSelectionChangedEvent> = this._register(new Emitter<ICursorSelectionChangedEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeCursorSelection: Event<ICursorSelectionChangedEvent> = this._onDidChangeCursorSelection.event; - private readonly _onDidAttemptReadOnlyEdit: Emitter<void> = this._register(new Emitter<void>()); + private readonly _onDidAttemptReadOnlyEdit: Emitter<void> = this._register(new Emitter<void>({ deliveryQueue: this._deliveryQueue })); public readonly onDidAttemptReadOnlyEdit: Event<void> = this._onDidAttemptReadOnlyEdit.event; - private readonly _onDidLayoutChange: Emitter<EditorLayoutInfo> = this._register(new Emitter<EditorLayoutInfo>()); + private readonly _onDidLayoutChange: Emitter<EditorLayoutInfo> = this._register(new Emitter<EditorLayoutInfo>({ deliveryQueue: this._deliveryQueue })); public readonly onDidLayoutChange: Event<EditorLayoutInfo> = this._onDidLayoutChange.event; - private readonly _editorTextFocus: BooleanEventEmitter = this._register(new BooleanEventEmitter()); + private readonly _editorTextFocus: BooleanEventEmitter = this._register(new BooleanEventEmitter({ deliveryQueue: this._deliveryQueue })); public readonly onDidFocusEditorText: Event<void> = this._editorTextFocus.onDidChangeToTrue; public readonly onDidBlurEditorText: Event<void> = this._editorTextFocus.onDidChangeToFalse; - private readonly _editorWidgetFocus: BooleanEventEmitter = this._register(new BooleanEventEmitter()); + private readonly _editorWidgetFocus: BooleanEventEmitter = this._register(new BooleanEventEmitter({ deliveryQueue: this._deliveryQueue })); public readonly onDidFocusEditorWidget: Event<void> = this._editorWidgetFocus.onDidChangeToTrue; public readonly onDidBlurEditorWidget: Event<void> = this._editorWidgetFocus.onDidChangeToFalse; - private readonly _onWillType: Emitter<string> = this._register(new Emitter<string>()); + private readonly _onWillType: Emitter<string> = this._register(new Emitter<string>({ deliveryQueue: this._deliveryQueue })); public readonly onWillType = this._onWillType.event; - private readonly _onDidType: Emitter<string> = this._register(new Emitter<string>()); + private readonly _onDidType: Emitter<string> = this._register(new Emitter<string>({ deliveryQueue: this._deliveryQueue })); public readonly onDidType = this._onDidType.event; - private readonly _onDidCompositionStart: Emitter<void> = this._register(new Emitter<void>()); + private readonly _onDidCompositionStart: Emitter<void> = this._register(new Emitter<void>({ deliveryQueue: this._deliveryQueue })); public readonly onDidCompositionStart = this._onDidCompositionStart.event; - private readonly _onDidCompositionEnd: Emitter<void> = this._register(new Emitter<void>()); + private readonly _onDidCompositionEnd: Emitter<void> = this._register(new Emitter<void>({ deliveryQueue: this._deliveryQueue })); public readonly onDidCompositionEnd = this._onDidCompositionEnd.event; - private readonly _onDidPaste: Emitter<editorBrowser.IPasteEvent> = this._register(new Emitter<editorBrowser.IPasteEvent>()); + private readonly _onDidPaste: Emitter<editorBrowser.IPasteEvent> = this._register(new Emitter<editorBrowser.IPasteEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onDidPaste = this._onDidPaste.event; - private readonly _onMouseUp: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>()); + private readonly _onMouseUp: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onMouseUp: Event<editorBrowser.IEditorMouseEvent> = this._onMouseUp.event; - private readonly _onMouseDown: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>()); + private readonly _onMouseDown: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onMouseDown: Event<editorBrowser.IEditorMouseEvent> = this._onMouseDown.event; - private readonly _onMouseDrag: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>()); + private readonly _onMouseDrag: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onMouseDrag: Event<editorBrowser.IEditorMouseEvent> = this._onMouseDrag.event; - private readonly _onMouseDrop: Emitter<editorBrowser.IPartialEditorMouseEvent> = this._register(new Emitter<editorBrowser.IPartialEditorMouseEvent>()); + private readonly _onMouseDrop: Emitter<editorBrowser.IPartialEditorMouseEvent> = this._register(new Emitter<editorBrowser.IPartialEditorMouseEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onMouseDrop: Event<editorBrowser.IPartialEditorMouseEvent> = this._onMouseDrop.event; - private readonly _onMouseDropCanceled: Emitter<void> = this._register(new Emitter<void>()); + private readonly _onMouseDropCanceled: Emitter<void> = this._register(new Emitter<void>({ deliveryQueue: this._deliveryQueue })); public readonly onMouseDropCanceled: Event<void> = this._onMouseDropCanceled.event; - private readonly _onDropIntoEditor = this._register(new Emitter<{ readonly position: IPosition; readonly event: DragEvent }>()); + private readonly _onDropIntoEditor = this._register(new Emitter<{ readonly position: IPosition; readonly event: DragEvent }>({ deliveryQueue: this._deliveryQueue })); public readonly onDropIntoEditor = this._onDropIntoEditor.event; - private readonly _onContextMenu: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>()); + private readonly _onContextMenu: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onContextMenu: Event<editorBrowser.IEditorMouseEvent> = this._onContextMenu.event; - private readonly _onMouseMove: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>()); + private readonly _onMouseMove: Emitter<editorBrowser.IEditorMouseEvent> = this._register(new Emitter<editorBrowser.IEditorMouseEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onMouseMove: Event<editorBrowser.IEditorMouseEvent> = this._onMouseMove.event; - private readonly _onMouseLeave: Emitter<editorBrowser.IPartialEditorMouseEvent> = this._register(new Emitter<editorBrowser.IPartialEditorMouseEvent>()); + private readonly _onMouseLeave: Emitter<editorBrowser.IPartialEditorMouseEvent> = this._register(new Emitter<editorBrowser.IPartialEditorMouseEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onMouseLeave: Event<editorBrowser.IPartialEditorMouseEvent> = this._onMouseLeave.event; - private readonly _onMouseWheel: Emitter<IMouseWheelEvent> = this._register(new Emitter<IMouseWheelEvent>()); + private readonly _onMouseWheel: Emitter<IMouseWheelEvent> = this._register(new Emitter<IMouseWheelEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onMouseWheel: Event<IMouseWheelEvent> = this._onMouseWheel.event; - private readonly _onKeyUp: Emitter<IKeyboardEvent> = this._register(new Emitter<IKeyboardEvent>()); + private readonly _onKeyUp: Emitter<IKeyboardEvent> = this._register(new Emitter<IKeyboardEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onKeyUp: Event<IKeyboardEvent> = this._onKeyUp.event; - private readonly _onKeyDown: Emitter<IKeyboardEvent> = this._register(new Emitter<IKeyboardEvent>()); + private readonly _onKeyDown: Emitter<IKeyboardEvent> = this._register(new Emitter<IKeyboardEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event; - private readonly _onDidContentSizeChange: Emitter<editorCommon.IContentSizeChangedEvent> = this._register(new Emitter<editorCommon.IContentSizeChangedEvent>()); + private readonly _onDidContentSizeChange: Emitter<editorCommon.IContentSizeChangedEvent> = this._register(new Emitter<editorCommon.IContentSizeChangedEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onDidContentSizeChange: Event<editorCommon.IContentSizeChangedEvent> = this._onDidContentSizeChange.event; - private readonly _onDidScrollChange: Emitter<editorCommon.IScrollEvent> = this._register(new Emitter<editorCommon.IScrollEvent>()); + private readonly _onDidScrollChange: Emitter<editorCommon.IScrollEvent> = this._register(new Emitter<editorCommon.IScrollEvent>({ deliveryQueue: this._deliveryQueue })); public readonly onDidScrollChange: Event<editorCommon.IScrollEvent> = this._onDidScrollChange.event; - private readonly _onDidChangeViewZones: Emitter<void> = this._register(new Emitter<void>()); + private readonly _onDidChangeViewZones: Emitter<void> = this._register(new Emitter<void>({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeViewZones: Event<void> = this._onDidChangeViewZones.event; - private readonly _onDidChangeHiddenAreas: Emitter<void> = this._register(new Emitter<void>()); + private readonly _onDidChangeHiddenAreas: Emitter<void> = this._register(new Emitter<void>({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeHiddenAreas: Event<void> = this._onDidChangeHiddenAreas.event; //#endregion @@ -1840,15 +1843,17 @@ const enum BooleanEventValue { } export class BooleanEventEmitter extends Disposable { - private readonly _onDidChangeToTrue: Emitter<void> = this._register(new Emitter<void>()); + private readonly _onDidChangeToTrue: Emitter<void> = this._register(new Emitter<void>(this._emitterOptions)); public readonly onDidChangeToTrue: Event<void> = this._onDidChangeToTrue.event; - private readonly _onDidChangeToFalse: Emitter<void> = this._register(new Emitter<void>()); + private readonly _onDidChangeToFalse: Emitter<void> = this._register(new Emitter<void>(this._emitterOptions)); public readonly onDidChangeToFalse: Event<void> = this._onDidChangeToFalse.event; private _value: BooleanEventValue; - constructor() { + constructor( + private readonly _emitterOptions: EmitterOptions + ) { super(); this._value = BooleanEventValue.NotSet; } diff --git a/src/vs/editor/common/commands/shiftCommand.ts b/src/vs/editor/common/commands/shiftCommand.ts index e9c6d2633bb..1352d7a60f9 100644 --- a/src/vs/editor/common/commands/shiftCommand.ts +++ b/src/vs/editor/common/commands/shiftCommand.ts @@ -10,8 +10,9 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection, SelectionDirection } from 'vs/editor/common/core/selection'; import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; +import { getEnterAction } from 'vs/editor/common/languages/enterAction'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; export interface IShiftCommandOpts { isUnshift: boolean; @@ -79,7 +80,11 @@ export class ShiftCommand implements ICommand { private _useLastEditRangeForCursorEndPosition: boolean; private _selectionStartColumnStaysPut: boolean; - constructor(range: Selection, opts: IShiftCommandOpts) { + constructor( + range: Selection, + opts: IShiftCommandOpts, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService + ) { this._opts = opts; this._selection = range; this._selectionId = null; @@ -142,7 +147,7 @@ export class ShiftCommand implements ICommand { // 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)) { - const enterAction = LanguageConfigurationRegistry.getEnterAction(this._opts.autoIndent, model, new Range(lineNumber - 1, model.getLineMaxColumn(lineNumber - 1), lineNumber - 1, model.getLineMaxColumn(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; if (enterAction.appendText) { diff --git a/src/vs/editor/common/config/editorConfigurationSchema.ts b/src/vs/editor/common/config/editorConfigurationSchema.ts index 288b0842dbe..650f20b7047 100644 --- a/src/vs/editor/common/config/editorConfigurationSchema.ts +++ b/src/vs/editor/common/config/editorConfigurationSchema.ts @@ -96,8 +96,8 @@ const editorConfiguration: IConfigurationNode = { description: nls.localize('maxTokenizationLineLength', "Lines above this length will not be tokenized for performance reasons") }, 'editor.language.brackets': { - type: 'array', - default: false, // We want to distinguish the empty array from not configured. + type: ['array', 'null'], + default: null, // We want to distinguish the empty array from not configured. description: nls.localize('schema.brackets', 'Defines the bracket symbols that increase or decrease the indentation.'), items: { type: 'array', @@ -114,8 +114,8 @@ const editorConfiguration: IConfigurationNode = { } }, 'editor.language.colorizedBracketPairs': { - type: 'array', - default: false, // We want to distinguish the empty array from not configured. + type: ['array', 'null'], + default: null, // We want to distinguish the empty array from not configured. description: nls.localize('schema.colorizedBracketPairs', 'Defines the bracket pairs that are colorized by their nesting level if bracket pair colorization is enabled.'), items: { type: 'array', diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index cad37a1c110..2d38b52d373 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -2514,6 +2514,13 @@ export interface IEditorInlayHintsOptions { * Defaults to editor font family. */ fontFamily?: string; + + /** + * The display style to render inlay hints with. + * Compact mode disables the borders and padding around the inlay hint. + * Defaults to 'standard'. + */ + displayStyle: 'standard' | 'compact'; } /** @@ -2524,7 +2531,7 @@ export type EditorInlayHintsOptions = Readonly<Required<IEditorInlayHintsOptions class EditorInlayHints extends BaseEditorOption<EditorOption.inlayHints, IEditorInlayHintsOptions, EditorInlayHintsOptions> { constructor() { - const defaults: EditorInlayHintsOptions = { enabled: true, fontSize: 0, fontFamily: '' }; + const defaults: EditorInlayHintsOptions = { enabled: true, fontSize: 0, fontFamily: '', displayStyle: 'compact' }; super( EditorOption.inlayHints, 'inlayHints', defaults, { @@ -2536,13 +2543,23 @@ class EditorInlayHints extends BaseEditorOption<EditorOption.inlayHints, IEditor 'editor.inlayHints.fontSize': { type: 'number', default: defaults.fontSize, - markdownDescription: nls.localize('inlayHints.fontSize', "Controls font size of inlay hints in the editor. A default of 90% of `#editor.fontSize#` is used when the configured value is less than `5` or greater than the editor font size.") + markdownDescription: nls.localize('inlayHints.fontSize', "Controls font size of inlay hints in the editor. As default the `#editor.fontSize#` is used when the configured value is less than `5` or greater than the editor font size.") }, 'editor.inlayHints.fontFamily': { type: 'string', default: defaults.fontFamily, markdownDescription: nls.localize('inlayHints.fontFamily', "Controls font family of inlay hints in the editor. When set to empty, the `#editor.fontFamily#` is used.") }, + 'editor.inlayHints.displayStyle': { + type: 'string', + enum: ['standard', 'compact'], + enumDescriptions: [ + nls.localize('inlayHints.displayStyle.standard', "Renders inlay hints with the default style."), + nls.localize('inlayHints.displayStyle.compact', "Renders inlay hints without any padding, and removes the rounded borders."), + ], + default: defaults.displayStyle, + description: nls.localize('inlayHints.displayStyle', "Controls the display style of inlay hints.") + } } ); } @@ -2555,7 +2572,8 @@ class EditorInlayHints extends BaseEditorOption<EditorOption.inlayHints, IEditor return { enabled: boolean(input.enabled, this.defaultValue.enabled), fontSize: EditorIntOption.clampedInt(input.fontSize, this.defaultValue.fontSize, 0, 100), - fontFamily: EditorStringOption.string(input.fontFamily, this.defaultValue.fontFamily) + fontFamily: EditorStringOption.string(input.fontFamily, this.defaultValue.fontFamily), + displayStyle: stringSet<'standard' | 'compact'>(input.displayStyle, this.defaultValue.displayStyle, ['standard', 'compact']) }; } } diff --git a/src/vs/editor/common/cursor/cursorMoveOperations.ts b/src/vs/editor/common/cursor/cursorMoveOperations.ts index 3e7dc921f89..5d089066f6d 100644 --- a/src/vs/editor/common/cursor/cursorMoveOperations.ts +++ b/src/vs/editor/common/cursor/cursorMoveOperations.ts @@ -197,7 +197,7 @@ export class MoveOperations { } public static down(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number, leftoverVisibleColumns: number, count: number, allowMoveOnLastLine: boolean): CursorPosition { - return this.vertical(config, model, lineNumber, column, leftoverVisibleColumns, lineNumber + count, allowMoveOnLastLine, PositionAffinity.Right); + return this.vertical(config, model, lineNumber, column, leftoverVisibleColumns, lineNumber + count, allowMoveOnLastLine, PositionAffinity.RightOfInjectedText); } public static moveDown(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, linesCount: number): SingleCursorState { @@ -233,7 +233,7 @@ export class MoveOperations { } public static up(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number, leftoverVisibleColumns: number, count: number, allowMoveOnFirstLine: boolean): CursorPosition { - return this.vertical(config, model, lineNumber, column, leftoverVisibleColumns, lineNumber - count, allowMoveOnFirstLine, PositionAffinity.Left); + return this.vertical(config, model, lineNumber, column, leftoverVisibleColumns, lineNumber - count, allowMoveOnFirstLine, PositionAffinity.LeftOfInjectedText); } public static moveUp(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, linesCount: number): SingleCursorState { diff --git a/src/vs/editor/common/cursor/cursorTypeOperations.ts b/src/vs/editor/common/cursor/cursorTypeOperations.ts index bcb5de92a42..d53f8d89ccf 100644 --- a/src/vs/editor/common/cursor/cursorTypeOperations.ts +++ b/src/vs/editor/common/cursor/cursorTypeOperations.ts @@ -17,10 +17,12 @@ import { Position } from 'vs/editor/common/core/position'; import { ICommand, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { EnterAction, IndentAction, StandardAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { getIndentationAtPosition } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IElectricAction } from 'vs/editor/common/languages/supports/electricCharacter'; import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; import { createScopedLineTokens } from 'vs/editor/common/languages/supports'; +import { getIndentActionForType, getIndentForEnter, getInheritIndentForLine } from 'vs/editor/common/languages/autoIndent'; +import { getEnterAction } from 'vs/editor/common/languages/enterAction'; export class TypeOperations { @@ -38,7 +40,7 @@ export class TypeOperations { insertSpaces: config.insertSpaces, useTabStops: config.useTabStops, autoIndent: config.autoIndent - }); + }, config.languageConfigurationService); } return commands; } @@ -53,7 +55,7 @@ export class TypeOperations { insertSpaces: config.insertSpaces, useTabStops: config.useTabStops, autoIndent: config.autoIndent - }); + }, config.languageConfigurationService); } return commands; } @@ -153,7 +155,7 @@ export class TypeOperations { let action: IndentAction | EnterAction | null = null; let indentation: string = ''; - const expectedIndentAction = LanguageConfigurationRegistry.getInheritIndentForLine(config.autoIndent, model, lineNumber, false); + const expectedIndentAction = getInheritIndentForLine(config.autoIndent, model, lineNumber, false, config.languageConfigurationService); if (expectedIndentAction) { action = expectedIndentAction.action; indentation = expectedIndentAction.indentation; @@ -173,7 +175,7 @@ export class TypeOperations { } const maxColumn = model.getLineMaxColumn(lastLineNumber); - const expectedEnterAction = LanguageConfigurationRegistry.getEnterAction(config.autoIndent, model, new Range(lastLineNumber, maxColumn, lastLineNumber, maxColumn)); + const expectedEnterAction = getEnterAction(config.autoIndent, model, new Range(lastLineNumber, maxColumn, lastLineNumber, maxColumn), config.languageConfigurationService); if (expectedEnterAction) { indentation = expectedEnterAction.indentation + expectedEnterAction.appendText; } @@ -253,7 +255,7 @@ export class TypeOperations { insertSpaces: config.insertSpaces, useTabStops: config.useTabStops, autoIndent: config.autoIndent - }); + }, config.languageConfigurationService); } } return commands; @@ -304,7 +306,7 @@ export class TypeOperations { return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); } - const r = LanguageConfigurationRegistry.getEnterAction(config.autoIndent, model, range); + const r = getEnterAction(config.autoIndent, model, range, config.languageConfigurationService); if (r) { if (r.indentAction === IndentAction.None) { // Nothing special @@ -336,7 +338,7 @@ export class TypeOperations { const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); if (config.autoIndent >= EditorAutoIndentStrategy.Full) { - const ir = LanguageConfigurationRegistry.getIndentForEnter(config.autoIndent, model, range, { + const ir = getIndentForEnter(config.autoIndent, model, range, { unshiftIndent: (indent) => { return TypeOperations.unshiftIndent(config, indent); }, @@ -346,7 +348,7 @@ export class TypeOperations { normalizeIndentation: (indent) => { return config.normalizeIndentation(indent); } - }); + }, config.languageConfigurationService); if (ir) { let oldEndViewColumn = config.visibleColumnFromColumn(model, range.getEndPosition()); @@ -392,15 +394,15 @@ export class TypeOperations { } private static _runAutoIndentType(config: CursorConfiguration, model: ITextModel, range: Range, ch: string): ICommand | null { - const currentIndentation = LanguageConfigurationRegistry.getIndentationAtPosition(model, range.startLineNumber, range.startColumn); - const actualIndentation = LanguageConfigurationRegistry.getIndentActionForType(config.autoIndent, model, range, ch, { + const currentIndentation = getIndentationAtPosition(model, range.startLineNumber, range.startColumn); + const actualIndentation = getIndentActionForType(config.autoIndent, model, range, ch, { shiftIndent: (indentation) => { return TypeOperations.shiftIndent(config, indentation); }, unshiftIndent: (indentation) => { return TypeOperations.unshiftIndent(config, indentation); }, - }); + }, config.languageConfigurationService); if (actualIndentation === null) { return null; diff --git a/src/vs/editor/common/cursorCommon.ts b/src/vs/editor/common/cursorCommon.ts index 4ea742f6ac8..4318d619c80 100644 --- a/src/vs/editor/common/cursorCommon.ts +++ b/src/vs/editor/common/cursorCommon.ts @@ -107,7 +107,7 @@ export class CursorConfiguration { languageId: string, modelOptions: TextModelResolvedOptions, configuration: IEditorConfiguration, - private readonly languageConfigurationService: ILanguageConfigurationService + public readonly languageConfigurationService: ILanguageConfigurationService ) { this._languageId = languageId; diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 5cbf14c62ad..de092a4d1be 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1293,6 +1293,11 @@ export interface DocumentSymbolProvider { export type TextEdit = { range: IRange; text: string; eol?: model.EndOfLineSequence }; +export interface SnippetTextEdit { + range: IRange; + snippet: string; +} + /** * Interface used to format a model */ @@ -1963,3 +1968,23 @@ export enum ExternalUriOpenerPriority { Default = 2, Preferred = 3, } + +/** + * @internal + */ +export interface IDataTransferItem { + asString(): Thenable<string>; + value: any; +} + +/** + * @internal + */ +export type IDataTransfer = Map<string, IDataTransferItem>; + +/** + * @internal + */ +export interface DocumentOnDropEditProvider { + provideDocumentOnDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IDataTransfer, token: CancellationToken): ProviderResult<SnippetTextEdit>; +} diff --git a/src/vs/editor/common/languages/autoIndent.ts b/src/vs/editor/common/languages/autoIndent.ts new file mode 100644 index 00000000000..d588ea766a8 --- /dev/null +++ b/src/vs/editor/common/languages/autoIndent.ts @@ -0,0 +1,435 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as strings from 'vs/base/common/strings'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { IndentAction } from 'vs/editor/common/languages/languageConfiguration'; +import { createScopedLineTokens } from 'vs/editor/common/languages/supports'; +import { IndentConsts, IndentRulesSupport } from 'vs/editor/common/languages/supports/indentRules'; +import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; +import { getScopedLineTokens, ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; + +export interface IVirtualModel { + getLineTokens(lineNumber: number): LineTokens; + getLanguageId(): string; + getLanguageIdAtPosition(lineNumber: number, column: number): string; + getLineContent(lineNumber: number): string; +} + +export interface IIndentConverter { + shiftIndent(indentation: string): string; + unshiftIndent(indentation: string): string; + normalizeIndentation?(indentation: string): string; +} + +/** + * Get nearest preceding line which doesn't match unIndentPattern or contains all whitespace. + * Result: + * -1: run into the boundary of embedded languages + * 0: every line above are invalid + * else: nearest preceding line of the same language + */ +function getPrecedingValidLine(model: IVirtualModel, lineNumber: number, indentRulesSupport: IndentRulesSupport) { + const languageId = model.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) { + return resultLineNumber; + } + const text = model.getLineContent(lastLineNumber); + if (indentRulesSupport.shouldIgnore(text) || /^\s+$/.test(text) || text === '') { + resultLineNumber = lastLineNumber; + continue; + } + + return lastLineNumber; + } + } + + return -1; +} + +/** + * Get inherited indentation from above lines. + * 1. Find the nearest preceding line which doesn't match unIndentedLinePattern. + * 2. If this line matches indentNextLinePattern or increaseIndentPattern, it means that the indent level of `lineNumber` should be 1 greater than this line. + * 3. If this line doesn't match any indent rules + * a. check whether the line above it matches indentNextLinePattern + * b. If not, the indent level of this line is the result + * c. If so, it means the indent of this line is *temporary*, go upward utill we find a line whose indent is not temporary (the same workflow a -> b -> c). + * 4. Otherwise, we fail to get an inherited indent from aboves. Return null and we should not touch the indent of `lineNumber` + * + * This function only return the inherited indent based on above lines, it doesn't check whether current line should decrease or not. + */ +export function getInheritIndentForLine( + autoIndent: EditorAutoIndentStrategy, + model: IVirtualModel, + lineNumber: number, + honorIntentialIndent: boolean = true, + languageConfigurationService: ILanguageConfigurationService +): { indentation: string; action: IndentAction | null; line?: number } | null { + if (autoIndent < EditorAutoIndentStrategy.Full) { + return null; + } + + const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentRulesSupport; + if (!indentRulesSupport) { + return null; + } + + if (lineNumber <= 1) { + return { + indentation: '', + action: null + }; + } + + const precedingUnIgnoredLine = getPrecedingValidLine(model, lineNumber, indentRulesSupport); + if (precedingUnIgnoredLine < 0) { + return null; + } else if (precedingUnIgnoredLine < 1) { + return { + indentation: '', + action: null + }; + } + + const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine); + if (indentRulesSupport.shouldIncrease(precedingUnIgnoredLineContent) || indentRulesSupport.shouldIndentNextLine(precedingUnIgnoredLineContent)) { + return { + indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent), + action: IndentAction.Indent, + line: precedingUnIgnoredLine + }; + } else if (indentRulesSupport.shouldDecrease(precedingUnIgnoredLineContent)) { + return { + indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent), + action: null, + line: precedingUnIgnoredLine + }; + } else { + // precedingUnIgnoredLine can not be ignored. + // it doesn't increase indent of following lines + // it doesn't increase just next line + // so current line is not affect by precedingUnIgnoredLine + // and then we should get a correct inheritted indentation from above lines + if (precedingUnIgnoredLine === 1) { + return { + indentation: strings.getLeadingWhitespace(model.getLineContent(precedingUnIgnoredLine)), + action: null, + line: precedingUnIgnoredLine + }; + } + + const previousLine = precedingUnIgnoredLine - 1; + + const previousLineIndentMetadata = indentRulesSupport.getIndentMetadata(model.getLineContent(previousLine)); + if (!(previousLineIndentMetadata & (IndentConsts.INCREASE_MASK | IndentConsts.DECREASE_MASK)) && + (previousLineIndentMetadata & IndentConsts.INDENT_NEXTLINE_MASK)) { + let stopLine = 0; + for (let i = previousLine - 1; i > 0; i--) { + if (indentRulesSupport.shouldIndentNextLine(model.getLineContent(i))) { + continue; + } + stopLine = i; + break; + } + + return { + indentation: strings.getLeadingWhitespace(model.getLineContent(stopLine + 1)), + action: null, + line: stopLine + 1 + }; + } + + if (honorIntentialIndent) { + return { + indentation: strings.getLeadingWhitespace(model.getLineContent(precedingUnIgnoredLine)), + action: null, + line: precedingUnIgnoredLine + }; + } else { + // search from precedingUnIgnoredLine until we find one whose indent is not temporary + for (let i = precedingUnIgnoredLine; i > 0; i--) { + const lineContent = model.getLineContent(i); + if (indentRulesSupport.shouldIncrease(lineContent)) { + return { + indentation: strings.getLeadingWhitespace(lineContent), + action: IndentAction.Indent, + line: i + }; + } else if (indentRulesSupport.shouldIndentNextLine(lineContent)) { + let stopLine = 0; + for (let j = i - 1; j > 0; j--) { + if (indentRulesSupport.shouldIndentNextLine(model.getLineContent(i))) { + continue; + } + stopLine = j; + break; + } + + return { + indentation: strings.getLeadingWhitespace(model.getLineContent(stopLine + 1)), + action: null, + line: stopLine + 1 + }; + } else if (indentRulesSupport.shouldDecrease(lineContent)) { + return { + indentation: strings.getLeadingWhitespace(lineContent), + action: null, + line: i + }; + } + } + + return { + indentation: strings.getLeadingWhitespace(model.getLineContent(1)), + action: null, + line: 1 + }; + } + } +} + +export function getGoodIndentForLine( + autoIndent: EditorAutoIndentStrategy, + virtualModel: IVirtualModel, + languageId: string, + lineNumber: number, + indentConverter: IIndentConverter, + languageConfigurationService: ILanguageConfigurationService +): string | null { + if (autoIndent < EditorAutoIndentStrategy.Full) { + return null; + } + + const richEditSupport = languageConfigurationService.getLanguageConfiguration(languageId); + if (!richEditSupport) { + return null; + } + + const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; + if (!indentRulesSupport) { + return null; + } + + const indent = getInheritIndentForLine(autoIndent, virtualModel, lineNumber, undefined, languageConfigurationService); + const lineContent = virtualModel.getLineContent(lineNumber); + + if (indent) { + const inheritLine = indent.line; + if (inheritLine !== undefined) { + const enterResult = richEditSupport.onEnter(autoIndent, '', virtualModel.getLineContent(inheritLine), ''); + + if (enterResult) { + let indentation = strings.getLeadingWhitespace(virtualModel.getLineContent(inheritLine)); + + if (enterResult.removeText) { + indentation = indentation.substring(0, indentation.length - enterResult.removeText); + } + + if ( + (enterResult.indentAction === IndentAction.Indent) || + (enterResult.indentAction === IndentAction.IndentOutdent) + ) { + indentation = indentConverter.shiftIndent(indentation); + } else if (enterResult.indentAction === IndentAction.Outdent) { + indentation = indentConverter.unshiftIndent(indentation); + } + + if (indentRulesSupport.shouldDecrease(lineContent)) { + indentation = indentConverter.unshiftIndent(indentation); + } + + if (enterResult.appendText) { + indentation += enterResult.appendText; + } + + return strings.getLeadingWhitespace(indentation); + } + } + + if (indentRulesSupport.shouldDecrease(lineContent)) { + if (indent.action === IndentAction.Indent) { + return indent.indentation; + } else { + return indentConverter.unshiftIndent(indent.indentation); + } + } else { + if (indent.action === IndentAction.Indent) { + return indentConverter.shiftIndent(indent.indentation); + } else { + return indent.indentation; + } + } + } + return null; +} + +export function getIndentForEnter( + autoIndent: EditorAutoIndentStrategy, + model: ITextModel, + range: Range, + indentConverter: IIndentConverter, + languageConfigurationService: ILanguageConfigurationService +): { beforeEnter: string; afterEnter: string } | null { + if (autoIndent < EditorAutoIndentStrategy.Full) { + return null; + } + model.forceTokenization(range.startLineNumber); + const lineTokens = model.getLineTokens(range.startLineNumber); + const scopedLineTokens = createScopedLineTokens(lineTokens, range.startColumn - 1); + const scopedLineText = scopedLineTokens.getLineContent(); + + let embeddedLanguage = false; + let beforeEnterText: string; + if (scopedLineTokens.firstCharOffset > 0 && lineTokens.getLanguageId(0) !== scopedLineTokens.languageId) { + // we are in the embeded language content + embeddedLanguage = true; // if embeddedLanguage is true, then we don't touch the indentation of current line + beforeEnterText = scopedLineText.substr(0, range.startColumn - 1 - scopedLineTokens.firstCharOffset); + } else { + beforeEnterText = lineTokens.getLineContent().substring(0, range.startColumn - 1); + } + + let afterEnterText: string; + if (range.isEmpty()) { + afterEnterText = scopedLineText.substr(range.startColumn - 1 - scopedLineTokens.firstCharOffset); + } else { + const endScopedLineTokens = getScopedLineTokens(model, range.endLineNumber, range.endColumn); + afterEnterText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); + } + + const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId).indentRulesSupport; + if (!indentRulesSupport) { + return null; + } + + const beforeEnterResult = beforeEnterText; + 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); + }, + getLineContent: (lineNumber: number) => { + if (lineNumber === range.startLineNumber) { + return beforeEnterResult; + } else { + return model.getLineContent(lineNumber); + } + } + }; + + const currentLineIndent = strings.getLeadingWhitespace(lineTokens.getLineContent()); + const afterEnterAction = getInheritIndentForLine(autoIndent, virtualModel, range.startLineNumber + 1, undefined, languageConfigurationService); + if (!afterEnterAction) { + const beforeEnter = embeddedLanguage ? currentLineIndent : beforeEnterIndent; + return { + beforeEnter: beforeEnter, + afterEnter: beforeEnter + }; + } + + let afterEnterIndent = embeddedLanguage ? currentLineIndent : afterEnterAction.indentation; + + if (afterEnterAction.action === IndentAction.Indent) { + afterEnterIndent = indentConverter.shiftIndent(afterEnterIndent); + } + + if (indentRulesSupport.shouldDecrease(afterEnterText)) { + afterEnterIndent = indentConverter.unshiftIndent(afterEnterIndent); + } + + return { + beforeEnter: embeddedLanguage ? currentLineIndent : beforeEnterIndent, + afterEnter: afterEnterIndent + }; +} + +/** + * We should always allow intentional indentation. It means, if users change the indentation of `lineNumber` and the content of + * this line doesn't match decreaseIndentPattern, we should not adjust the indentation. + */ +export function getIndentActionForType( + autoIndent: EditorAutoIndentStrategy, + model: ITextModel, + range: Range, + ch: string, + indentConverter: IIndentConverter, + languageConfigurationService: ILanguageConfigurationService +): string | null { + if (autoIndent < EditorAutoIndentStrategy.Full) { + return null; + } + const scopedLineTokens = getScopedLineTokens(model, range.startLineNumber, range.startColumn); + + if (scopedLineTokens.firstCharOffset) { + // this line has mixed languages and indentation rules will not work + return null; + } + + const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId).indentRulesSupport; + if (!indentRulesSupport) { + return null; + } + + const scopedLineText = scopedLineTokens.getLineContent(); + const beforeTypeText = scopedLineText.substr(0, range.startColumn - 1 - scopedLineTokens.firstCharOffset); + + // selection support + let afterTypeText: string; + if (range.isEmpty()) { + afterTypeText = scopedLineText.substr(range.startColumn - 1 - scopedLineTokens.firstCharOffset); + } else { + const endScopedLineTokens = getScopedLineTokens(model, range.endLineNumber, range.endColumn); + afterTypeText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); + } + + // If previous content already matches decreaseIndentPattern, it means indentation of this line should already be adjusted + // Users might change the indentation by purpose and we should honor that instead of readjusting. + if (!indentRulesSupport.shouldDecrease(beforeTypeText + afterTypeText) && indentRulesSupport.shouldDecrease(beforeTypeText + ch + afterTypeText)) { + // after typing `ch`, the content matches decreaseIndentPattern, we should adjust the indent to a good manner. + // 1. Get inherited indent action + const r = getInheritIndentForLine(autoIndent, model, range.startLineNumber, false, languageConfigurationService); + if (!r) { + return null; + } + + let indentation = r.indentation; + if (r.action !== IndentAction.Indent) { + indentation = indentConverter.unshiftIndent(indentation); + } + + return indentation; + } + + return null; +} + +export function getIndentMetadata( + model: ITextModel, + lineNumber: number, + languageConfigurationService: ILanguageConfigurationService +): number | null { + const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentRulesSupport; + if (!indentRulesSupport) { + return null; + } + if (lineNumber < 1 || lineNumber > model.getLineCount()) { + return null; + } + return indentRulesSupport.getIndentMetadata(model.getLineContent(lineNumber)); +} diff --git a/src/vs/editor/common/languages/enterAction.ts b/src/vs/editor/common/languages/enterAction.ts new file mode 100644 index 00000000000..447665fe816 --- /dev/null +++ b/src/vs/editor/common/languages/enterAction.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { IndentAction, CompleteEnterAction } from 'vs/editor/common/languages/languageConfiguration'; +import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; +import { getIndentationAtPosition, getScopedLineTokens, ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; + +export function getEnterAction( + autoIndent: EditorAutoIndentStrategy, + model: ITextModel, + range: Range, + languageConfigurationService: ILanguageConfigurationService +): CompleteEnterAction | null { + const scopedLineTokens = getScopedLineTokens(model, range.startLineNumber, range.startColumn); + const richEditSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId); + if (!richEditSupport) { + return null; + } + + const scopedLineText = scopedLineTokens.getLineContent(); + const beforeEnterText = scopedLineText.substr(0, range.startColumn - 1 - scopedLineTokens.firstCharOffset); + + // selection support + let afterEnterText: string; + if (range.isEmpty()) { + afterEnterText = scopedLineText.substr(range.startColumn - 1 - scopedLineTokens.firstCharOffset); + } else { + const endScopedLineTokens = getScopedLineTokens(model, range.endLineNumber, range.endColumn); + afterEnterText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); + } + + let previousLineText = ''; + if (range.startLineNumber > 1 && scopedLineTokens.firstCharOffset === 0) { + // This is not the first line and the entire line belongs to this mode + const oneLineAboveScopedLineTokens = getScopedLineTokens(model, range.startLineNumber - 1); + if (oneLineAboveScopedLineTokens.languageId === scopedLineTokens.languageId) { + // The line above ends with text belonging to the same mode + previousLineText = oneLineAboveScopedLineTokens.getLineContent(); + } + } + + const enterResult = richEditSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText); + if (!enterResult) { + return null; + } + + const indentAction = enterResult.indentAction; + let appendText = enterResult.appendText; + const removeText = enterResult.removeText || 0; + + // Here we add `\t` to appendText first because enterAction is leveraging appendText and removeText to change indentation. + if (!appendText) { + if ( + (indentAction === IndentAction.Indent) || + (indentAction === IndentAction.IndentOutdent) + ) { + appendText = '\t'; + } else { + appendText = ''; + } + } else if (indentAction === IndentAction.Indent) { + appendText = '\t' + appendText; + } + + let indentation = getIndentationAtPosition(model, range.startLineNumber, range.startColumn); + if (removeText) { + indentation = indentation.substring(0, indentation.length - removeText); + } + + return { + indentAction: indentAction, + appendText: appendText, + removeText: removeText, + indentation: indentation + }; +} diff --git a/src/vs/editor/common/languages/language.ts b/src/vs/editor/common/languages/language.ts index f18a8b2be9e..a6c22e78ae2 100644 --- a/src/vs/editor/common/languages/language.ts +++ b/src/vs/editor/common/languages/language.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ILanguageIdCodec } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -59,6 +60,11 @@ export interface ILanguageService { onDidChange: Event<void>; /** + * Register a language. + */ + registerLanguage(def: ILanguageExtensionPoint): IDisposable; + + /** * Check if `languageId` is registered. */ isRegisteredLanguageId(languageId: string): boolean; diff --git a/src/vs/editor/common/languages/languageConfigurationRegistry.ts b/src/vs/editor/common/languages/languageConfigurationRegistry.ts index 3b01f6cb43d..2b641d53c0e 100644 --- a/src/vs/editor/common/languages/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/languages/languageConfigurationRegistry.ts @@ -6,15 +6,13 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; -import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; -import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { DEFAULT_WORD_REGEXP, ensureValidWordDefinition } from 'vs/editor/common/core/wordHelper'; -import { EnterAction, FoldingRules, IAutoClosingPair, IndentAction, IndentationRule, LanguageConfiguration, CompleteEnterAction, AutoClosingPairs, CharacterPair, ExplicitLanguageConfiguration } from 'vs/editor/common/languages/languageConfiguration'; +import { EnterAction, FoldingRules, IAutoClosingPair, IndentationRule, LanguageConfiguration, AutoClosingPairs, CharacterPair, ExplicitLanguageConfiguration } from 'vs/editor/common/languages/languageConfiguration'; import { createScopedLineTokens, ScopedLineTokens } from 'vs/editor/common/languages/supports'; import { CharacterPairSupport } from 'vs/editor/common/languages/supports/characterPair'; import { BracketElectricCharacterSupport } from 'vs/editor/common/languages/supports/electricCharacter'; -import { IndentConsts, IndentRulesSupport } from 'vs/editor/common/languages/supports/indentRules'; +import { IndentRulesSupport } from 'vs/editor/common/languages/supports/indentRules'; import { OnEnterSupport } from 'vs/editor/common/languages/supports/onEnter'; import { RichEditBrackets } from 'vs/editor/common/languages/supports/richEditBrackets'; import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; @@ -22,6 +20,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; /** * Interface used to support insertion of mode specific comments. @@ -32,24 +31,18 @@ export interface ICommentsConfiguration { blockCommentEndToken?: string; } -export interface IVirtualModel { - getLineTokens(lineNumber: number): LineTokens; - getLanguageId(): string; - getLanguageIdAtPosition(lineNumber: number, column: number): string; - getLineContent(lineNumber: number): string; -} - -export interface IIndentConverter { - shiftIndent(indentation: string): string; - unshiftIndent(indentation: string): string; - normalizeIndentation?(indentation: string): string; -} - export interface ILanguageConfigurationService { readonly _serviceBrand: undefined; onDidChange: Event<LanguageConfigurationServiceChangeEvent>; + + /** + * @param priority Use a higher number for higher priority + */ + register(languageId: string, configuration: LanguageConfiguration, priority?: number): IDisposable; + getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration; + } export class LanguageConfigurationServiceChangeEvent { @@ -65,6 +58,8 @@ export const ILanguageConfigurationService = createDecorator<ILanguageConfigurat export class LanguageConfigurationService extends Disposable implements ILanguageConfigurationService { _serviceBrand: undefined; + private readonly _registry = this._register(new LanguageConfigurationRegistry()); + private readonly onDidChangeEmitter = this._register(new Emitter<LanguageConfigurationServiceChangeEvent>()); public readonly onDidChange = this.onDidChangeEmitter.event; @@ -101,16 +96,20 @@ export class LanguageConfigurationService extends Disposable implements ILanguag } })); - this._register(LanguageConfigurationRegistry.onDidChange((e) => { + this._register(this._registry.onDidChange((e) => { this.configurations.delete(e.languageId); this.onDidChangeEmitter.fire(new LanguageConfigurationServiceChangeEvent(e.languageId)); })); } + public register(languageId: string, configuration: LanguageConfiguration, priority?: number): IDisposable { + return this._registry.register(languageId, configuration, priority); + } + public getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration { let result = this.configurations.get(languageId); if (!result) { - result = computeConfig(languageId, this.configurationService, this.languageService); + result = computeConfig(languageId, this._registry, this.configurationService, this.languageService); this.configurations.set(languageId, result); } return result; @@ -119,10 +118,11 @@ export class LanguageConfigurationService extends Disposable implements ILanguag function computeConfig( languageId: string, + registry: LanguageConfigurationRegistry, configurationService: IConfigurationService, languageService: ILanguageService, ): ResolvedLanguageConfiguration { - let languageConfig = LanguageConfigurationRegistry.getLanguageConfiguration(languageId); + let languageConfig = registry.getLanguageConfiguration(languageId); if (!languageConfig) { if (!languageService.isRegisteredLanguageId(languageId)) { @@ -169,540 +169,21 @@ function validateBracketPairs(data: unknown): CharacterPair[] | undefined { }).filter((p): p is CharacterPair => !!p); } -export class LanguageConfigurationChangeEvent { - constructor(public readonly languageId: string) { } -} - -export class LanguageConfigurationRegistryImpl { - private readonly _entries = new Map<string, ComposedLanguageConfiguration>(); - - private readonly _onDidChange = new Emitter<LanguageConfigurationChangeEvent>(); - public readonly onDidChange: Event<LanguageConfigurationChangeEvent> = this._onDidChange.event; - - /** - * @param priority Use a higher number for higher priority - */ - public register(languageId: string, configuration: LanguageConfiguration, priority: number = 0): IDisposable { - let entries = this._entries.get(languageId); - if (!entries) { - entries = new ComposedLanguageConfiguration(languageId); - this._entries.set(languageId, entries); - } - - const disposable = entries.register(configuration, priority); - this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageId)); - - return toDisposable(() => { - disposable.dispose(); - this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageId)); - }); - } - - public getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration | null { - const entries = this._entries.get(languageId); - return entries?.getResolvedConfiguration() || null; - } - - public getComments(languageId: string): ICommentsConfiguration | null { - const value = this.getLanguageConfiguration(languageId); - if (!value) { - return null; - } - return value.comments || null; - } - - // begin Indent Rules - - public getIndentRulesSupport(languageId: string): IndentRulesSupport | null { - const value = this.getLanguageConfiguration(languageId); - if (!value) { - return null; - } - return value.indentRulesSupport || null; - } - - /** - * Get nearest preceding line which doesn't match unIndentPattern or contains all whitespace. - * Result: - * -1: run into the boundary of embedded languages - * 0: every line above are invalid - * else: nearest preceding line of the same language - */ - private getPrecedingValidLine(model: IVirtualModel, lineNumber: number, indentRulesSupport: IndentRulesSupport) { - const languageId = model.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) { - return resultLineNumber; - } - const text = model.getLineContent(lastLineNumber); - if (indentRulesSupport.shouldIgnore(text) || /^\s+$/.test(text) || text === '') { - resultLineNumber = lastLineNumber; - continue; - } - - return lastLineNumber; - } - } - - return -1; - } - - /** - * Get inherited indentation from above lines. - * 1. Find the nearest preceding line which doesn't match unIndentedLinePattern. - * 2. If this line matches indentNextLinePattern or increaseIndentPattern, it means that the indent level of `lineNumber` should be 1 greater than this line. - * 3. If this line doesn't match any indent rules - * a. check whether the line above it matches indentNextLinePattern - * b. If not, the indent level of this line is the result - * c. If so, it means the indent of this line is *temporary*, go upward utill we find a line whose indent is not temporary (the same workflow a -> b -> c). - * 4. Otherwise, we fail to get an inherited indent from aboves. Return null and we should not touch the indent of `lineNumber` - * - * This function only return the inherited indent based on above lines, it doesn't check whether current line should decrease or not. - */ - public getInheritIndentForLine(autoIndent: EditorAutoIndentStrategy, model: IVirtualModel, lineNumber: number, honorIntentialIndent: boolean = true): { indentation: string; action: IndentAction | null; line?: number } | null { - if (autoIndent < EditorAutoIndentStrategy.Full) { - return null; - } - - const indentRulesSupport = this.getIndentRulesSupport(model.getLanguageId()); - if (!indentRulesSupport) { - return null; - } - - if (lineNumber <= 1) { - return { - indentation: '', - action: null - }; - } - - const precedingUnIgnoredLine = this.getPrecedingValidLine(model, lineNumber, indentRulesSupport); - if (precedingUnIgnoredLine < 0) { - return null; - } else if (precedingUnIgnoredLine < 1) { - return { - indentation: '', - action: null - }; - } - - const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine); - if (indentRulesSupport.shouldIncrease(precedingUnIgnoredLineContent) || indentRulesSupport.shouldIndentNextLine(precedingUnIgnoredLineContent)) { - return { - indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent), - action: IndentAction.Indent, - line: precedingUnIgnoredLine - }; - } else if (indentRulesSupport.shouldDecrease(precedingUnIgnoredLineContent)) { - return { - indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent), - action: null, - line: precedingUnIgnoredLine - }; - } else { - // precedingUnIgnoredLine can not be ignored. - // it doesn't increase indent of following lines - // it doesn't increase just next line - // so current line is not affect by precedingUnIgnoredLine - // and then we should get a correct inheritted indentation from above lines - if (precedingUnIgnoredLine === 1) { - return { - indentation: strings.getLeadingWhitespace(model.getLineContent(precedingUnIgnoredLine)), - action: null, - line: precedingUnIgnoredLine - }; - } - - const previousLine = precedingUnIgnoredLine - 1; - - const previousLineIndentMetadata = indentRulesSupport.getIndentMetadata(model.getLineContent(previousLine)); - if (!(previousLineIndentMetadata & (IndentConsts.INCREASE_MASK | IndentConsts.DECREASE_MASK)) && - (previousLineIndentMetadata & IndentConsts.INDENT_NEXTLINE_MASK)) { - let stopLine = 0; - for (let i = previousLine - 1; i > 0; i--) { - if (indentRulesSupport.shouldIndentNextLine(model.getLineContent(i))) { - continue; - } - stopLine = i; - break; - } - - return { - indentation: strings.getLeadingWhitespace(model.getLineContent(stopLine + 1)), - action: null, - line: stopLine + 1 - }; - } - - if (honorIntentialIndent) { - return { - indentation: strings.getLeadingWhitespace(model.getLineContent(precedingUnIgnoredLine)), - action: null, - line: precedingUnIgnoredLine - }; - } else { - // search from precedingUnIgnoredLine until we find one whose indent is not temporary - for (let i = precedingUnIgnoredLine; i > 0; i--) { - const lineContent = model.getLineContent(i); - if (indentRulesSupport.shouldIncrease(lineContent)) { - return { - indentation: strings.getLeadingWhitespace(lineContent), - action: IndentAction.Indent, - line: i - }; - } else if (indentRulesSupport.shouldIndentNextLine(lineContent)) { - let stopLine = 0; - for (let j = i - 1; j > 0; j--) { - if (indentRulesSupport.shouldIndentNextLine(model.getLineContent(i))) { - continue; - } - stopLine = j; - break; - } - - return { - indentation: strings.getLeadingWhitespace(model.getLineContent(stopLine + 1)), - action: null, - line: stopLine + 1 - }; - } else if (indentRulesSupport.shouldDecrease(lineContent)) { - return { - indentation: strings.getLeadingWhitespace(lineContent), - action: null, - line: i - }; - } - } - - return { - indentation: strings.getLeadingWhitespace(model.getLineContent(1)), - action: null, - line: 1 - }; - } - } - } - - public getGoodIndentForLine(autoIndent: EditorAutoIndentStrategy, virtualModel: IVirtualModel, languageId: string, lineNumber: number, indentConverter: IIndentConverter): string | null { - if (autoIndent < EditorAutoIndentStrategy.Full) { - return null; - } - - const richEditSupport = this.getLanguageConfiguration(languageId); - if (!richEditSupport) { - return null; - } - - const indentRulesSupport = this.getIndentRulesSupport(languageId); - if (!indentRulesSupport) { - return null; - } - - const indent = this.getInheritIndentForLine(autoIndent, virtualModel, lineNumber); - const lineContent = virtualModel.getLineContent(lineNumber); - - if (indent) { - const inheritLine = indent.line; - if (inheritLine !== undefined) { - const enterResult = richEditSupport.onEnter(autoIndent, '', virtualModel.getLineContent(inheritLine), ''); - - if (enterResult) { - let indentation = strings.getLeadingWhitespace(virtualModel.getLineContent(inheritLine)); - - if (enterResult.removeText) { - indentation = indentation.substring(0, indentation.length - enterResult.removeText); - } - - if ( - (enterResult.indentAction === IndentAction.Indent) || - (enterResult.indentAction === IndentAction.IndentOutdent) - ) { - indentation = indentConverter.shiftIndent(indentation); - } else if (enterResult.indentAction === IndentAction.Outdent) { - indentation = indentConverter.unshiftIndent(indentation); - } - - if (indentRulesSupport.shouldDecrease(lineContent)) { - indentation = indentConverter.unshiftIndent(indentation); - } - - if (enterResult.appendText) { - indentation += enterResult.appendText; - } - - return strings.getLeadingWhitespace(indentation); - } - } - - if (indentRulesSupport.shouldDecrease(lineContent)) { - if (indent.action === IndentAction.Indent) { - return indent.indentation; - } else { - return indentConverter.unshiftIndent(indent.indentation); - } - } else { - if (indent.action === IndentAction.Indent) { - return indentConverter.shiftIndent(indent.indentation); - } else { - return indent.indentation; - } - } - } - return null; - } - - public getIndentForEnter(autoIndent: EditorAutoIndentStrategy, model: ITextModel, range: Range, indentConverter: IIndentConverter): { beforeEnter: string; afterEnter: string } | null { - if (autoIndent < EditorAutoIndentStrategy.Full) { - return null; - } - model.forceTokenization(range.startLineNumber); - const lineTokens = model.getLineTokens(range.startLineNumber); - const scopedLineTokens = createScopedLineTokens(lineTokens, range.startColumn - 1); - const scopedLineText = scopedLineTokens.getLineContent(); - - let embeddedLanguage = false; - let beforeEnterText: string; - if (scopedLineTokens.firstCharOffset > 0 && lineTokens.getLanguageId(0) !== scopedLineTokens.languageId) { - // we are in the embeded language content - embeddedLanguage = true; // if embeddedLanguage is true, then we don't touch the indentation of current line - beforeEnterText = scopedLineText.substr(0, range.startColumn - 1 - scopedLineTokens.firstCharOffset); - } else { - beforeEnterText = lineTokens.getLineContent().substring(0, range.startColumn - 1); - } - - let afterEnterText: string; - if (range.isEmpty()) { - afterEnterText = scopedLineText.substr(range.startColumn - 1 - scopedLineTokens.firstCharOffset); - } else { - const endScopedLineTokens = this.getScopedLineTokens(model, range.endLineNumber, range.endColumn); - afterEnterText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); - } - - const indentRulesSupport = this.getIndentRulesSupport(scopedLineTokens.languageId); - if (!indentRulesSupport) { - return null; - } - - const beforeEnterResult = beforeEnterText; - 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); - }, - getLineContent: (lineNumber: number) => { - if (lineNumber === range.startLineNumber) { - return beforeEnterResult; - } else { - return model.getLineContent(lineNumber); - } - } - }; - - const currentLineIndent = strings.getLeadingWhitespace(lineTokens.getLineContent()); - const afterEnterAction = this.getInheritIndentForLine(autoIndent, virtualModel, range.startLineNumber + 1); - if (!afterEnterAction) { - const beforeEnter = embeddedLanguage ? currentLineIndent : beforeEnterIndent; - return { - beforeEnter: beforeEnter, - afterEnter: beforeEnter - }; - } - - let afterEnterIndent = embeddedLanguage ? currentLineIndent : afterEnterAction.indentation; - - if (afterEnterAction.action === IndentAction.Indent) { - afterEnterIndent = indentConverter.shiftIndent(afterEnterIndent); - } - - if (indentRulesSupport.shouldDecrease(afterEnterText)) { - afterEnterIndent = indentConverter.unshiftIndent(afterEnterIndent); - } - - return { - beforeEnter: embeddedLanguage ? currentLineIndent : beforeEnterIndent, - afterEnter: afterEnterIndent - }; - } - - /** - * We should always allow intentional indentation. It means, if users change the indentation of `lineNumber` and the content of - * this line doesn't match decreaseIndentPattern, we should not adjust the indentation. - */ - public getIndentActionForType(autoIndent: EditorAutoIndentStrategy, model: ITextModel, range: Range, ch: string, indentConverter: IIndentConverter): string | null { - if (autoIndent < EditorAutoIndentStrategy.Full) { - return null; - } - const scopedLineTokens = this.getScopedLineTokens(model, range.startLineNumber, range.startColumn); - - if (scopedLineTokens.firstCharOffset) { - // this line has mixed languages and indentation rules will not work - return null; - } - - const indentRulesSupport = this.getIndentRulesSupport(scopedLineTokens.languageId); - if (!indentRulesSupport) { - return null; - } - - const scopedLineText = scopedLineTokens.getLineContent(); - const beforeTypeText = scopedLineText.substr(0, range.startColumn - 1 - scopedLineTokens.firstCharOffset); - - // selection support - let afterTypeText: string; - if (range.isEmpty()) { - afterTypeText = scopedLineText.substr(range.startColumn - 1 - scopedLineTokens.firstCharOffset); - } else { - const endScopedLineTokens = this.getScopedLineTokens(model, range.endLineNumber, range.endColumn); - afterTypeText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); - } - - // If previous content already matches decreaseIndentPattern, it means indentation of this line should already be adjusted - // Users might change the indentation by purpose and we should honor that instead of readjusting. - if (!indentRulesSupport.shouldDecrease(beforeTypeText + afterTypeText) && indentRulesSupport.shouldDecrease(beforeTypeText + ch + afterTypeText)) { - // after typing `ch`, the content matches decreaseIndentPattern, we should adjust the indent to a good manner. - // 1. Get inherited indent action - const r = this.getInheritIndentForLine(autoIndent, model, range.startLineNumber, false); - if (!r) { - return null; - } - - let indentation = r.indentation; - if (r.action !== IndentAction.Indent) { - indentation = indentConverter.unshiftIndent(indentation); - } - - return indentation; - } - - return null; - } - - public getIndentMetadata(model: ITextModel, lineNumber: number): number | null { - const indentRulesSupport = this.getIndentRulesSupport(model.getLanguageId()); - if (!indentRulesSupport) { - return null; - } - if (lineNumber < 1 || lineNumber > model.getLineCount()) { - return null; - } - return indentRulesSupport.getIndentMetadata(model.getLineContent(lineNumber)); - } - - // end Indent Rules - - // begin onEnter - - public getEnterAction(autoIndent: EditorAutoIndentStrategy, model: ITextModel, range: Range): CompleteEnterAction | null { - const scopedLineTokens = this.getScopedLineTokens(model, range.startLineNumber, range.startColumn); - const richEditSupport = this.getLanguageConfiguration(scopedLineTokens.languageId); - if (!richEditSupport) { - return null; - } - - const scopedLineText = scopedLineTokens.getLineContent(); - const beforeEnterText = scopedLineText.substr(0, range.startColumn - 1 - scopedLineTokens.firstCharOffset); - - // selection support - let afterEnterText: string; - if (range.isEmpty()) { - afterEnterText = scopedLineText.substr(range.startColumn - 1 - scopedLineTokens.firstCharOffset); - } else { - const endScopedLineTokens = this.getScopedLineTokens(model, range.endLineNumber, range.endColumn); - afterEnterText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); - } - - let previousLineText = ''; - if (range.startLineNumber > 1 && scopedLineTokens.firstCharOffset === 0) { - // This is not the first line and the entire line belongs to this mode - const oneLineAboveScopedLineTokens = this.getScopedLineTokens(model, range.startLineNumber - 1); - if (oneLineAboveScopedLineTokens.languageId === scopedLineTokens.languageId) { - // The line above ends with text belonging to the same mode - previousLineText = oneLineAboveScopedLineTokens.getLineContent(); - } - } - - const enterResult = richEditSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText); - if (!enterResult) { - return null; - } - - const indentAction = enterResult.indentAction; - let appendText = enterResult.appendText; - const removeText = enterResult.removeText || 0; - - // Here we add `\t` to appendText first because enterAction is leveraging appendText and removeText to change indentation. - if (!appendText) { - if ( - (indentAction === IndentAction.Indent) || - (indentAction === IndentAction.IndentOutdent) - ) { - appendText = '\t'; - } else { - appendText = ''; - } - } else if (indentAction === IndentAction.Indent) { - appendText = '\t' + appendText; - } - - let indentation = this.getIndentationAtPosition(model, range.startLineNumber, range.startColumn); - if (removeText) { - indentation = indentation.substring(0, indentation.length - removeText); - } - - return { - indentAction: indentAction, - appendText: appendText, - removeText: removeText, - indentation: indentation - }; - } - - public getIndentationAtPosition(model: ITextModel, lineNumber: number, column: number): string { - const lineText = model.getLineContent(lineNumber); - let indentation = strings.getLeadingWhitespace(lineText); - if (indentation.length > column - 1) { - indentation = indentation.substring(0, column - 1); - } - return indentation; - } - - private getScopedLineTokens(model: ITextModel, lineNumber: number, columnNumber?: number): ScopedLineTokens { - model.forceTokenization(lineNumber); - const lineTokens = model.getLineTokens(lineNumber); - const column = (typeof columnNumber === 'undefined' ? model.getLineMaxColumn(lineNumber) - 1 : columnNumber - 1); - return createScopedLineTokens(lineTokens, column); - } - - // end onEnter - - public getBracketsSupport(languageId: string): RichEditBrackets | null { - const value = this.getLanguageConfiguration(languageId); - if (!value) { - return null; - } - return value.brackets || null; - } - - public getColorizedBracketPairs(languageId: string): readonly CharacterPair[] { - return this.getLanguageConfiguration(languageId)?.characterPair.getColorizedBrackets() || []; +export function getIndentationAtPosition(model: ITextModel, lineNumber: number, column: number): string { + const lineText = model.getLineContent(lineNumber); + let indentation = strings.getLeadingWhitespace(lineText); + if (indentation.length > column - 1) { + indentation = indentation.substring(0, column - 1); } + return indentation; } -/** - * @deprecated Use ILanguageConfigurationService instead. -*/ -export const LanguageConfigurationRegistry = new LanguageConfigurationRegistryImpl(); +export function getScopedLineTokens(model: ITextModel, lineNumber: number, columnNumber?: number): ScopedLineTokens { + model.forceTokenization(lineNumber); + const lineTokens = model.getLineTokens(lineNumber); + const column = (typeof columnNumber === 'undefined' ? model.getLineMaxColumn(lineNumber) - 1 : columnNumber - 1); + return createScopedLineTokens(lineTokens, column); +} class ComposedLanguageConfiguration { private readonly _entries: LanguageConfigurationContribution[]; @@ -809,6 +290,65 @@ class LanguageConfigurationContribution { } } +export class LanguageConfigurationChangeEvent { + constructor(public readonly languageId: string) { } +} + +export class LanguageConfigurationRegistry extends Disposable { + private readonly _entries = new Map<string, ComposedLanguageConfiguration>(); + + private readonly _onDidChange = this._register(new Emitter<LanguageConfigurationChangeEvent>()); + public readonly onDidChange: Event<LanguageConfigurationChangeEvent> = this._onDidChange.event; + + constructor() { + super(); + this._register(this.register(PLAINTEXT_LANGUAGE_ID, { + brackets: [ + ['(', ')'], + ['[', ']'], + ['{', '}'], + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '<', close: '>' }, + { open: '\"', close: '\"' }, + { open: '\'', close: '\'' }, + { open: '`', close: '`' }, + ], + colorizedBracketPairs: [], + folding: { + offSide: true + } + }, 0)); + } + + /** + * @param priority Use a higher number for higher priority + */ + public register(languageId: string, configuration: LanguageConfiguration, priority: number = 0): IDisposable { + let entries = this._entries.get(languageId); + if (!entries) { + entries = new ComposedLanguageConfiguration(languageId); + this._entries.set(languageId, entries); + } + + const disposable = entries.register(configuration, priority); + this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageId)); + + return toDisposable(() => { + disposable.dispose(); + this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageId)); + }); + } + + public getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration | null { + const entries = this._entries.get(languageId); + return entries?.getResolvedConfiguration() || null; + } +} + /** * Immutable. */ diff --git a/src/vs/editor/common/languages/modesRegistry.ts b/src/vs/editor/common/languages/modesRegistry.ts index 4af8ae52311..7376d0c069b 100644 --- a/src/vs/editor/common/languages/modesRegistry.ts +++ b/src/vs/editor/common/languages/modesRegistry.ts @@ -5,7 +5,6 @@ import * as nls from 'vs/nls'; import { Emitter, Event } from 'vs/base/common/event'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ILanguageExtensionPoint } from 'vs/editor/common/languages/language'; import { Registry } from 'vs/platform/registry/common/platform'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -61,27 +60,6 @@ ModesRegistry.registerLanguage({ mimetypes: [Mimes.text] }); -LanguageConfigurationRegistry.register(PLAINTEXT_LANGUAGE_ID, { - brackets: [ - ['(', ')'], - ['[', ']'], - ['{', '}'], - ], - surroundingPairs: [ - { open: '{', close: '}' }, - { open: '[', close: ']' }, - { open: '(', close: ')' }, - { open: '<', close: '>' }, - { open: '\"', close: '\"' }, - { open: '\'', close: '\'' }, - { open: '`', close: '`' }, - ], - colorizedBracketPairs: [], - folding: { - offSide: true - } -}, 0); - Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration) .registerDefaultConfigurations([{ overrides: { diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index c35d8f1ba20..4d3ec0b94c8 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -1270,6 +1270,16 @@ export const enum PositionAffinity { * No preference. */ None = 2, + + /** + * If the given position is on injected text, prefers the position left of it. + */ + LeftOfInjectedText = 3, + + /** + * If the given position is on injected text, prefers the position right of it. + */ + RightOfInjectedText = 4, } /** @@ -1311,6 +1321,8 @@ export class ValidAnnotatedEditOperation implements IIdentifiedSingleEditOperati /** * @internal + * + * `lineNumber` is 1 based. */ export interface IReadonlyTextBuffer { onDidChangeContent: Event<void>; diff --git a/src/vs/editor/common/model/guidesTextModelPart.ts b/src/vs/editor/common/model/guidesTextModelPart.ts index 91e34291e9c..488ee8e79d4 100644 --- a/src/vs/editor/common/model/guidesTextModelPart.ts +++ b/src/vs/editor/common/model/guidesTextModelPart.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ArrayQueue, findLast } from 'vs/base/common/arrays'; +import { findLast } from 'vs/base/common/arrays'; import * as strings from 'vs/base/common/strings'; import { CursorColumns } from 'vs/editor/common/core/cursorColumns'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { BracketPairInfo } from 'vs/editor/common/textModelBracketPairs'; import type { TextModel } from 'vs/editor/common/model/textModel'; import { TextModelPart } from 'vs/editor/common/model/textModelPart'; import { computeIndentLevel } from 'vs/editor/common/model/utils'; @@ -278,6 +277,13 @@ export class GuidesTextModelPart extends TextModelPart implements IGuidesTextMod options: BracketGuideOptions ): IndentGuide[][] { const result: IndentGuide[][] = []; + for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { + result.push([]); + } + + // If requested, this could be made configurable. + const includeSingleLinePairs = true; + const bracketPairs = this.textModel.bracketPairs.getBracketPairsInRangeWithMinIndentation( new Range( @@ -302,205 +308,151 @@ export class GuidesTextModelPart extends TextModelPart implements IGuidesTextMod activeBracketPairRange = findLast( bracketsContainingActivePosition, - /* Exclude single line bracket pairs for cases such as - * ``` - * function test() { - * if (true) { | } - * } - * ``` - */ - (i) => i.range.startLineNumber !== i.range.endLineNumber + (i) => includeSingleLinePairs || i.range.startLineNumber !== i.range.endLineNumber )?.range; } - const queue = new ArrayQueue(bracketPairs); - /** Indexed by nesting level */ - const activeGuides = new Array<{ - nestingLevel: number; - nestingLevelOfEqualBracketType: number; - guideVisibleColumn: number; - start: Position; - visibleStartColumn: number; - end: Position; - visibleEndColumn: number; - bracketPair: BracketPairInfo; - renderHorizontalEndLineAtTheBottom: boolean; - } | null>(); - const nextGuides = new Array<IndentGuide>(); + const independentColorPoolPerBracketType = this.textModel.getOptions().bracketPairColorizationOptions.independentColorPoolPerBracketType; const colorProvider = new BracketPairGuidesClassNames(); - const independentColorPoolPerBracketType = this.textModel.getOptions().bracketPairColorizationOptions - .independentColorPoolPerBracketType; - for ( - let lineNumber = startLineNumber; - lineNumber <= endLineNumber; - lineNumber++ - ) { - let guides = new Array<IndentGuide>(); - if (nextGuides.length > 0) { - guides = guides.concat(nextGuides); - nextGuides.length = 0; + for (const pair of bracketPairs) { + /* + + + { + | + } + + { + | + ----} + + ____{ + |test + ----} + + renderHorizontalEndLineAtTheBottom: + { + | + |x} + -- + renderHorizontalEndLineAtTheBottom: + ____{ + |test + | x } + ---- + */ + + const isActive = activeBracketPairRange && pair.range.equalsRange(activeBracketPairRange); + + if (!isActive && !options.includeInactive) { + continue; } - result.push(guides); - - // Update activeGuides - for (const pair of queue.takeWhile( - (b) => b.openingBracketRange.startLineNumber <= lineNumber - ) || []) { - if (pair.range.startLineNumber === pair.range.endLineNumber) { - // ignore single line brackets - continue; - } - const guideVisibleColumn = Math.min( - this.getVisibleColumnFromPosition( - pair.openingBracketRange.getStartPosition() - ), - this.getVisibleColumnFromPosition( - pair.closingBracketRange?.getStartPosition() ?? - pair.range.getEndPosition() - ), - pair.minVisibleColumnIndentation + 1 - ); - let renderHorizontalEndLineAtTheBottom = false; - if (pair.closingBracketRange) { - const firstNonWsIndex = strings.firstNonWhitespaceIndex( - this.textModel.getLineContent( - pair.closingBracketRange.startLineNumber + + const className = + colorProvider.getInlineClassName(pair.nestingLevel, pair.nestingLevelOfEqualBracketType, independentColorPoolPerBracketType) + + (options.highlightActive && isActive + ? ' ' + colorProvider.activeClassName + : ''); + + + const start = pair.openingBracketRange.getStartPosition(); + const end = + pair.closingBracketRange?.getStartPosition() ?? + pair.range.getEndPosition(); + + const horizontalGuides = options.horizontalGuides === HorizontalGuidesState.Enabled || (options.horizontalGuides === HorizontalGuidesState.EnabledForActive && isActive); + + if (pair.range.startLineNumber === pair.range.endLineNumber) { + if (includeSingleLinePairs && horizontalGuides) { + + result[pair.range.startLineNumber - startLineNumber].push( + new IndentGuide( + -1, + pair.openingBracketRange.getEndPosition().column, + className, + new IndentGuideHorizontalLine(false, end.column), + -1, + -1, ) ); - if (firstNonWsIndex < pair.closingBracketRange.startColumn - 1) { - renderHorizontalEndLineAtTheBottom = true; - } - } - const start = pair.openingBracketRange.getStartPosition(); - const end = - pair.closingBracketRange?.getStartPosition() ?? - pair.range.getEndPosition(); - - if (pair.closingBracketRange === undefined) { - // Don't show guides for bracket pairs that are not balanced. - // See #135125. - activeGuides[pair.nestingLevel] = null; - } else { - activeGuides[pair.nestingLevel] = { - nestingLevel: pair.nestingLevel, - nestingLevelOfEqualBracketType: pair.nestingLevelOfEqualBracketType, - guideVisibleColumn, - start, - visibleStartColumn: this.getVisibleColumnFromPosition(start), - end, - visibleEndColumn: this.getVisibleColumnFromPosition(end), - bracketPair: pair, - renderHorizontalEndLineAtTheBottom, - }; } + continue; } - for (const line of activeGuides) { - if (!line) { - continue; - } - const isActive = - activeBracketPairRange && - line.bracketPair.range.equalsRange(activeBracketPairRange); - - const className = - colorProvider.getInlineClassName(line.nestingLevel, line.nestingLevelOfEqualBracketType, independentColorPoolPerBracketType) + - (options.highlightActive && isActive - ? ' ' + colorProvider.activeClassName - : ''); + const endVisibleColumn = this.getVisibleColumnFromPosition(end); + const startVisibleColumn = this.getVisibleColumnFromPosition( + pair.openingBracketRange.getStartPosition() + ); + const guideVisibleColumn = Math.min(startVisibleColumn, endVisibleColumn, pair.minVisibleColumnIndentation + 1); - if ( - (isActive && - options.horizontalGuides !== - HorizontalGuidesState.Disabled) || - (options.includeInactive && - options.horizontalGuides === HorizontalGuidesState.Enabled) - ) { - if (line.start.lineNumber === lineNumber) { - if (line.guideVisibleColumn < line.visibleStartColumn) { - guides.push( - new IndentGuide( - line.guideVisibleColumn, - className, - new IndentGuideHorizontalLine(false, line.start.column) - ) - ); - } - } - if (line.end.lineNumber === lineNumber + 1) { - // The next line might have horizontal guides. - // However, the next line might also have a new bracket pair with the same indentation, - // so the current bracket pair might get replaced. That's why we push the guide to nextGuides one line ahead. - if (line.guideVisibleColumn < line.visibleEndColumn) { - nextGuides.push( - new IndentGuide( - line.guideVisibleColumn, - className, - new IndentGuideHorizontalLine( - !line.renderHorizontalEndLineAtTheBottom, - line.end.column - ) - ) - ); - } - } + let renderHorizontalEndLineAtTheBottom = false; + if (pair.closingBracketRange) { + const firstNonWsIndex = strings.firstNonWhitespaceIndex( + this.textModel.getLineContent( + pair.closingBracketRange.startLineNumber + ) + ); + const hasTextBeforeClosingBracket = firstNonWsIndex < pair.closingBracketRange.startColumn - 1; + if (hasTextBeforeClosingBracket) { + renderHorizontalEndLineAtTheBottom = true; } } - let lastVisibleColumnCount = Number.MAX_SAFE_INTEGER; - // Going backwards, so the last guide potentially replaces others - for (let i = activeGuides.length - 1; i >= 0; i--) { - const line = activeGuides[i]; - if (!line) { - continue; - } - const isActive = - options.highlightActive && - activeBracketPairRange && - line.bracketPair.range.equalsRange(activeBracketPairRange); - - const className = - colorProvider.getInlineClassName( - line.nestingLevel, - line.nestingLevelOfEqualBracketType, - independentColorPoolPerBracketType - ) + (isActive ? ' ' + colorProvider.activeClassName : ''); - - if (isActive || options.includeInactive) { - if ( - line.renderHorizontalEndLineAtTheBottom && - line.end.lineNumber === lineNumber + 1 - ) { - nextGuides.push( - new IndentGuide(line.guideVisibleColumn, className, null) - ); - } - } - if ( - line.end.lineNumber <= lineNumber || - line.start.lineNumber >= lineNumber - ) { - continue; - } + const visibleGuideStartLineNumber = Math.max(start.lineNumber, startLineNumber); + const visibleGuideEndLineNumber = Math.min(end.lineNumber, endLineNumber); - if (line.guideVisibleColumn >= lastVisibleColumnCount && !isActive) { - // Don't render a guide on top of an existing guide, unless it is active. - continue; + const offset = renderHorizontalEndLineAtTheBottom ? 1 : 0; + + for (let l = visibleGuideStartLineNumber; l < visibleGuideEndLineNumber + offset; l++) { + result[l - startLineNumber].push( + new IndentGuide( + guideVisibleColumn, + -1, + className, + null, + l === start.lineNumber ? start.column : -1, + // TODO: Investigate if this is correct + l === end.lineNumber ? end.column : -1 + ) + ); + } + + if (horizontalGuides) { + if (start.lineNumber >= startLineNumber && startVisibleColumn > guideVisibleColumn) { + result[start.lineNumber - startLineNumber].push( + new IndentGuide( + guideVisibleColumn, + -1, + className, + new IndentGuideHorizontalLine(false, start.column), + -1, + -1, + ) + ); } - lastVisibleColumnCount = line.guideVisibleColumn; - if (isActive || options.includeInactive) { - guides.push( - new IndentGuide(line.guideVisibleColumn, className, null) + if (end.lineNumber <= endLineNumber && endVisibleColumn > guideVisibleColumn) { + result[end.lineNumber - startLineNumber].push( + new IndentGuide( + guideVisibleColumn, + -1, + className, + new IndentGuideHorizontalLine(!renderHorizontalEndLineAtTheBottom, end.column), + -1, + -1, + ) ); } } + } + for (const guides of result) { guides.sort((a, b) => a.visibleColumn - b.visibleColumn); } + return result; } diff --git a/src/vs/editor/common/modelLineProjectionData.ts b/src/vs/editor/common/modelLineProjectionData.ts index 3c51b88491b..8a6fbd6ca4b 100644 --- a/src/vs/editor/common/modelLineProjectionData.ts +++ b/src/vs/editor/common/modelLineProjectionData.ts @@ -229,7 +229,7 @@ export class ModelLineProjectionData { return result; } - } else if (affinity === PositionAffinity.Right) { + } else if (affinity === PositionAffinity.Right || affinity === PositionAffinity.RightOfInjectedText) { let result = injectedText.offsetInInputWithInjections + injectedText.length; let index = injectedText.injectedTextIndex; // traverse all injected text that touch each other @@ -238,7 +238,7 @@ export class ModelLineProjectionData { index++; } return result; - } else if (affinity === PositionAffinity.Left) { + } else if (affinity === PositionAffinity.Left || affinity === PositionAffinity.LeftOfInjectedText) { // affinity is left let result = injectedText.offsetInInputWithInjections; let index = injectedText.injectedTextIndex; diff --git a/src/vs/editor/common/services/languageFeatures.ts b/src/vs/editor/common/services/languageFeatures.ts index 14bd7532635..e0ba2416582 100644 --- a/src/vs/editor/common/services/languageFeatures.ts +++ b/src/vs/editor/common/services/languageFeatures.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { LanguageFeatureRegistry, NotebookInfoResolver } from 'vs/editor/common/languageFeatureRegistry'; -import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider } from 'vs/editor/common/languages'; +import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const ILanguageFeaturesService = createDecorator<ILanguageFeaturesService>('ILanguageFeaturesService'); @@ -67,6 +67,8 @@ export interface ILanguageFeaturesService { readonly evaluatableExpressionProvider: LanguageFeatureRegistry<EvaluatableExpressionProvider>; + readonly documentOnDropEditProvider: LanguageFeatureRegistry<DocumentOnDropEditProvider>; + // -- setNotebookTypeResolver(resolver: NotebookInfoResolver | undefined): void; diff --git a/src/vs/editor/common/services/languageFeaturesService.ts b/src/vs/editor/common/services/languageFeaturesService.ts index 92f57d892a7..2ed7cebcf62 100644 --- a/src/vs/editor/common/services/languageFeaturesService.ts +++ b/src/vs/editor/common/services/languageFeaturesService.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { LanguageFeatureRegistry, NotebookInfo, NotebookInfoResolver } from 'vs/editor/common/languageFeatureRegistry'; -import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider } from 'vs/editor/common/languages'; +import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider } from 'vs/editor/common/languages'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -40,7 +40,7 @@ export class LanguageFeaturesService implements ILanguageFeaturesService { readonly evaluatableExpressionProvider = new LanguageFeatureRegistry<EvaluatableExpressionProvider>(this._score.bind(this)); readonly documentRangeSemanticTokensProvider = new LanguageFeatureRegistry<DocumentRangeSemanticTokensProvider>(this._score.bind(this)); readonly documentSemanticTokensProvider = new LanguageFeatureRegistry<DocumentSemanticTokensProvider>(this._score.bind(this)); - + readonly documentOnDropEditProvider = new LanguageFeatureRegistry<DocumentOnDropEditProvider>(this._score.bind(this)); private _notebookTypeResolver?: NotebookInfoResolver; diff --git a/src/vs/editor/common/services/languageService.ts b/src/vs/editor/common/services/languageService.ts index 1ca2505b7c2..9dbbcda9b42 100644 --- a/src/vs/editor/common/services/languageService.ts +++ b/src/vs/editor/common/services/languageService.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { LanguagesRegistry } from 'vs/editor/common/services/languagesRegistry'; -import { ILanguageNameIdPair, ILanguageSelection, ILanguageService, ILanguageIcon } from 'vs/editor/common/languages/language'; +import { ILanguageNameIdPair, ILanguageSelection, ILanguageService, ILanguageIcon, ILanguageExtensionPoint } from 'vs/editor/common/languages/language'; import { firstOrDefault } from 'vs/base/common/arrays'; import { ILanguageIdCodec, TokenizationRegistry } from 'vs/editor/common/languages'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; @@ -41,6 +41,10 @@ export class LanguageService extends Disposable implements ILanguageService { super.dispose(); } + public registerLanguage(def: ILanguageExtensionPoint): IDisposable { + return this._registry.registerLanguage(def); + } + public isRegisteredLanguageId(languageId: string | null | undefined): boolean { return this._registry.isRegisteredLanguageId(languageId); } diff --git a/src/vs/editor/common/services/languagesRegistry.ts b/src/vs/editor/common/services/languagesRegistry.ts index 01b58ce512c..dba3c0b9bb6 100644 --- a/src/vs/editor/common/services/languagesRegistry.ts +++ b/src/vs/editor/common/services/languagesRegistry.ts @@ -5,7 +5,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { compareIgnoreCase, regExpLeadsToEndlessLoop } from 'vs/base/common/strings'; import { clearPlatformLanguageAssociations, getLanguageIds, registerPlatformLanguageAssociation } from 'vs/editor/common/services/languagesAssociations'; import { URI } from 'vs/base/common/uri'; @@ -14,13 +14,10 @@ import { ModesRegistry, PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages import { ILanguageExtensionPoint, ILanguageNameIdPair, ILanguageIcon } from 'vs/editor/common/languages/language'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; const hasOwnProperty = Object.prototype.hasOwnProperty; const NULL_LANGUAGE_ID = 'vs.editor.nullLanguage'; -LanguageConfigurationRegistry.register(NULL_LANGUAGE_ID, {}); - export interface IResolvedLanguage { identifier: string; name: string | null; @@ -122,6 +119,10 @@ export class LanguagesRegistry extends Disposable { this._registerLanguages(desc); } + registerLanguage(desc: ILanguageExtensionPoint): IDisposable { + return ModesRegistry.registerLanguage(desc); + } + _registerLanguages(desc: ILanguageExtensionPoint[]): void { for (const d of desc) { diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 6f3f214c702..265cb543054 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -707,7 +707,15 @@ export enum PositionAffinity { /** * No preference. */ - None = 2 + None = 2, + /** + * If the given position is on injected text, prefers the position left of it. + */ + LeftOfInjectedText = 3, + /** + * If the given position is on injected text, prefers the position right of it. + */ + RightOfInjectedText = 4 } export enum RenderLineNumbersType { diff --git a/src/vs/editor/common/textModelGuides.ts b/src/vs/editor/common/textModelGuides.ts index 5d36491e391..7b63ee6b4d1 100644 --- a/src/vs/editor/common/textModelGuides.ts +++ b/src/vs/editor/common/textModelGuides.ts @@ -17,6 +17,8 @@ export interface IGuidesTextModelPart { getLinesIndentGuides(startLineNumber: number, endLineNumber: number): number[]; /** + * Requests the the indent guides for the given range of lines. + * `result[i]` will contain the indent guides of the `startLineNumber + i`th line. * @internal */ getLinesBracketGuides(startLineNumber: number, endLineNumber: number, activePosition: IPosition | null, options: BracketGuideOptions): IndentGuide[][]; @@ -42,14 +44,21 @@ export interface BracketGuideOptions { export class IndentGuide { constructor( - public readonly visibleColumn: number, + public readonly visibleColumn: number | -1, + public readonly column: number | -1, public readonly className: string, /** * If set, this indent guide is a horizontal guide (no vertical part). * It starts at visibleColumn and continues until endColumn. */ public readonly horizontalLine: IndentGuideHorizontalLine | null, - ) { } + public readonly forWrappedLinesAfterColumn: number | -1, + public readonly forWrappedLinesBeforeColumn: number | -1 + ) { + if ((visibleColumn !== -1) === (column !== -1)) { + throw new Error(); + } + } } export class IndentGuideHorizontalLine { diff --git a/src/vs/editor/common/viewModel/viewModelLines.ts b/src/vs/editor/common/viewModel/viewModelLines.ts index 7f5063f2cd3..2dbfff15b9e 100644 --- a/src/vs/editor/common/viewModel/viewModelLines.ts +++ b/src/vs/editor/common/viewModel/viewModelLines.ts @@ -472,6 +472,14 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { ); } + private getMaxColumnOfViewLine(viewLineInfo: ViewLineInfo): number { + return this.modelLineProjections[viewLineInfo.modelLineNumber - 1].getViewLineMaxColumn( + this.model, + viewLineInfo.modelLineNumber, + viewLineInfo.modelLineWrappedLineIdx + ); + } + private getModelStartPositionOfViewLine(viewLineInfo: ViewLineInfo): Position { const line = this.modelLineProjections[viewLineInfo.modelLineNumber - 1]; const minViewColumn = line.getViewLineMinColumn( @@ -569,17 +577,71 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { // Don't add indent guides when the wrapped line continuation has no wrapping-indentation. resultPerViewLine.push([]); } else { - let bracketGuides = bracketGuidesPerModelLine[viewLineInfo.modelLineNumber - modelRangeStartLineNumber]; + const bracketGuides = bracketGuidesPerModelLine[viewLineInfo.modelLineNumber - modelRangeStartLineNumber]; // visibleColumns stay as they are (this is a bug and needs to be fixed, but it is not a regression) // model-columns must be converted to view-model columns. - bracketGuides = bracketGuides.map(g => g.horizontalLine ? - new IndentGuide(g.visibleColumn, g.className, - new IndentGuideHorizontalLine(g.horizontalLine.top, - this.convertModelPositionToViewPosition(viewLineInfo.modelLineNumber, g.horizontalLine.endColumn).column - ) - ) : g); - resultPerViewLine.push(bracketGuides); + const result = bracketGuides.map(g => { + /*if (g.onlyForWrappedLines && !viewLineInfo.isWrappedLineContinuation) { + return undefined; + }*/ + + if (g.forWrappedLinesAfterColumn !== -1) { + const p = this.modelLineProjections[viewLineInfo.modelLineNumber - 1].getViewPositionOfModelPosition(0, g.forWrappedLinesAfterColumn); + if (p.lineNumber >= viewLineInfo.modelLineWrappedLineIdx) { + return undefined; + } + } + + if (g.forWrappedLinesBeforeColumn !== -1) { + const p = this.modelLineProjections[viewLineInfo.modelLineNumber - 1].getViewPositionOfModelPosition(0, g.forWrappedLinesBeforeColumn); + if (p.lineNumber <= viewLineInfo.modelLineWrappedLineIdx) { + return undefined; + } + } + + if (!g.horizontalLine) { + return g; + } + + let column = -1; + if (g.column !== -1) { + const p = this.modelLineProjections[viewLineInfo.modelLineNumber - 1].getViewPositionOfModelPosition(0, g.column); + if (p.lineNumber === viewLineInfo.modelLineWrappedLineIdx) { + column = p.column; + } else if (p.lineNumber < viewLineInfo.modelLineWrappedLineIdx) { + column = this.getMinColumnOfViewLine(viewLineInfo); + } else if (p.lineNumber > viewLineInfo.modelLineWrappedLineIdx) { + return undefined; + } + } + + const viewPosition = this.convertModelPositionToViewPosition(viewLineInfo.modelLineNumber, g.horizontalLine.endColumn); + const p = this.modelLineProjections[viewLineInfo.modelLineNumber - 1].getViewPositionOfModelPosition(0, g.horizontalLine.endColumn); + if (p.lineNumber === viewLineInfo.modelLineWrappedLineIdx) { + return new IndentGuide(g.visibleColumn, column, g.className, + new IndentGuideHorizontalLine(g.horizontalLine.top, + viewPosition.column), + - 1, + -1, + ); + } else if (p.lineNumber < viewLineInfo.modelLineWrappedLineIdx) { + return undefined; + } else { + if (g.visibleColumn !== -1) { + // Don't repeat horizontal lines that use visibleColumn for unrelated lines. + return undefined; + } + return new IndentGuide(g.visibleColumn, column, g.className, + new IndentGuideHorizontalLine(g.horizontalLine.top, + this.getMaxColumnOfViewLine(viewLineInfo) + ), + -1, + -1, + ); + } + }); + resultPerViewLine.push(result.filter((r): r is IndentGuide => !!r)); } } } diff --git a/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts b/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts index 7db8c60c134..c2e738046d3 100644 --- a/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts +++ b/src/vs/editor/contrib/bracketMatching/test/browser/bracketMatching.test.ts @@ -5,138 +5,133 @@ import * as assert from 'assert'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { BracketMatchingController } from 'vs/editor/contrib/bracketMatching/browser/bracketMatching'; -import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; -import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; +import { createCodeEditorServices, instantiateTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { instantiateTextModel } from 'vs/editor/test/common/testTextModel'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILanguageService } from 'vs/editor/common/languages/language'; suite('bracket matching', () => { - class BracketMode extends MockMode { - constructor() { - super('bracketMode'); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'], - ] - })); - } + + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let languageConfigurationService: ILanguageConfigurationService; + let languageService: ILanguageService; + + setup(() => { + disposables = new DisposableStore(); + instantiationService = createCodeEditorServices(disposables); + languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + languageService = instantiationService.get(ILanguageService); + }); + + teardown(() => { + disposables.dispose(); + }); + + function createTextModelWithBrackets(text: string) { + const languageId = 'bracketMode'; + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ] + })); + return disposables.add(instantiateTextModel(instantiationService, text, languageId)); + } + + function createCodeEditorWithBrackets(text: string) { + return disposables.add(instantiateTestCodeEditor(instantiationService, createTextModelWithBrackets(text))); } test('issue #183: jump to matching bracket position', () => { - const mode = new BracketMode(); - const model = createTextModel('var x = (3 + (5-7)) + ((5+3)+5);', mode.languageId); - - withTestCodeEditor(model, {}, (editor) => { - const bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController); - - // start on closing bracket - editor.setPosition(new Position(1, 20)); - bracketMatchingController.jumpToBracket(); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); - bracketMatchingController.jumpToBracket(); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 19)); - bracketMatchingController.jumpToBracket(); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); - - // start on opening bracket - editor.setPosition(new Position(1, 23)); - bracketMatchingController.jumpToBracket(); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 31)); - bracketMatchingController.jumpToBracket(); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); - bracketMatchingController.jumpToBracket(); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 31)); - - bracketMatchingController.dispose(); - }); - - model.dispose(); - mode.dispose(); + const editor = createCodeEditorWithBrackets('var x = (3 + (5-7)) + ((5+3)+5);'); + const bracketMatchingController = disposables.add(editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController)); + + // start on closing bracket + editor.setPosition(new Position(1, 20)); + bracketMatchingController.jumpToBracket(); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); + bracketMatchingController.jumpToBracket(); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 19)); + bracketMatchingController.jumpToBracket(); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); + + // start on opening bracket + editor.setPosition(new Position(1, 23)); + bracketMatchingController.jumpToBracket(); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 31)); + bracketMatchingController.jumpToBracket(); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); + bracketMatchingController.jumpToBracket(); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 31)); }); test('Jump to next bracket', () => { - const mode = new BracketMode(); - const model = createTextModel('var x = (3 + (5-7)); y();', mode.languageId); - - withTestCodeEditor(model, {}, (editor) => { - const bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController); - - // start position between brackets - editor.setPosition(new Position(1, 16)); - bracketMatchingController.jumpToBracket(); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 18)); - bracketMatchingController.jumpToBracket(); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 14)); - bracketMatchingController.jumpToBracket(); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 18)); - - // skip brackets in comments - editor.setPosition(new Position(1, 21)); - bracketMatchingController.jumpToBracket(); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); - bracketMatchingController.jumpToBracket(); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 24)); - bracketMatchingController.jumpToBracket(); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); - - // do not break if no brackets are available - editor.setPosition(new Position(1, 26)); - bracketMatchingController.jumpToBracket(); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 26)); - - bracketMatchingController.dispose(); - }); - - model.dispose(); - mode.dispose(); + const editor = createCodeEditorWithBrackets('var x = (3 + (5-7)); y();'); + const bracketMatchingController = disposables.add(editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController)); + + // start position between brackets + editor.setPosition(new Position(1, 16)); + bracketMatchingController.jumpToBracket(); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 18)); + bracketMatchingController.jumpToBracket(); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 14)); + bracketMatchingController.jumpToBracket(); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 18)); + + // skip brackets in comments + editor.setPosition(new Position(1, 21)); + bracketMatchingController.jumpToBracket(); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); + bracketMatchingController.jumpToBracket(); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 24)); + bracketMatchingController.jumpToBracket(); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); + + // do not break if no brackets are available + editor.setPosition(new Position(1, 26)); + bracketMatchingController.jumpToBracket(); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 26)); }); test('Select to next bracket', () => { - const mode = new BracketMode(); - const model = createTextModel('var x = (3 + (5-7)); y();', mode.languageId); - - withTestCodeEditor(model, {}, (editor) => { - const bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController); - - - // start position in open brackets - editor.setPosition(new Position(1, 9)); - bracketMatchingController.selectToBracket(true); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 20)); - assert.deepStrictEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); - - // start position in close brackets (should select backwards) - editor.setPosition(new Position(1, 20)); - bracketMatchingController.selectToBracket(true); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); - assert.deepStrictEqual(editor.getSelection(), new Selection(1, 20, 1, 9)); - - // start position between brackets - editor.setPosition(new Position(1, 16)); - bracketMatchingController.selectToBracket(true); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 19)); - assert.deepStrictEqual(editor.getSelection(), new Selection(1, 14, 1, 19)); - - // start position outside brackets - editor.setPosition(new Position(1, 21)); - bracketMatchingController.selectToBracket(true); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 25)); - assert.deepStrictEqual(editor.getSelection(), new Selection(1, 23, 1, 25)); - - // do not break if no brackets are available - editor.setPosition(new Position(1, 26)); - bracketMatchingController.selectToBracket(true); - assert.deepStrictEqual(editor.getPosition(), new Position(1, 26)); - assert.deepStrictEqual(editor.getSelection(), new Selection(1, 26, 1, 26)); - - bracketMatchingController.dispose(); - }); - - model.dispose(); - mode.dispose(); + const editor = createCodeEditorWithBrackets('var x = (3 + (5-7)); y();'); + const bracketMatchingController = disposables.add(editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController)); + + // start position in open brackets + editor.setPosition(new Position(1, 9)); + bracketMatchingController.selectToBracket(true); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 20)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); + + // start position in close brackets (should select backwards) + editor.setPosition(new Position(1, 20)); + bracketMatchingController.selectToBracket(true); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 20, 1, 9)); + + // start position between brackets + editor.setPosition(new Position(1, 16)); + bracketMatchingController.selectToBracket(true); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 19)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 14, 1, 19)); + + // start position outside brackets + editor.setPosition(new Position(1, 21)); + bracketMatchingController.selectToBracket(true); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 25)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 23, 1, 25)); + + // do not break if no brackets are available + editor.setPosition(new Position(1, 26)); + bracketMatchingController.selectToBracket(true); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 26)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 26, 1, 26)); }); test('issue #1772: jump to enclosing brackets', () => { @@ -147,21 +142,12 @@ suite('bracket matching', () => { ' somethingmore: [0, 2, 4]', '};', ].join('\n'); - const mode = new BracketMode(); - const model = createTextModel(text, mode.languageId); - - withTestCodeEditor(model, {}, (editor) => { - const bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController); - - editor.setPosition(new Position(3, 5)); - bracketMatchingController.jumpToBracket(); - assert.deepStrictEqual(editor.getSelection(), new Selection(5, 1, 5, 1)); - - bracketMatchingController.dispose(); - }); + const editor = createCodeEditorWithBrackets(text); + const bracketMatchingController = disposables.add(editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController)); - model.dispose(); - mode.dispose(); + editor.setPosition(new Position(3, 5)); + bracketMatchingController.jumpToBracket(); + assert.deepStrictEqual(editor.getSelection(), new Selection(5, 1, 5, 1)); }); test('issue #43371: argument to not select brackets', () => { @@ -172,73 +158,55 @@ suite('bracket matching', () => { ' somethingmore: [0, 2, 4]', '};', ].join('\n'); - const mode = new BracketMode(); - const model = createTextModel(text, mode.languageId); - - withTestCodeEditor(model, {}, (editor) => { - const bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController); - - editor.setPosition(new Position(3, 5)); - bracketMatchingController.selectToBracket(false); - assert.deepStrictEqual(editor.getSelection(), new Selection(1, 12, 5, 1)); - - bracketMatchingController.dispose(); - }); + const editor = createCodeEditorWithBrackets(text); + const bracketMatchingController = disposables.add(editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController)); - model.dispose(); - mode.dispose(); + editor.setPosition(new Position(3, 5)); + bracketMatchingController.selectToBracket(false); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 12, 5, 1)); }); test('issue #45369: Select to Bracket with multicursor', () => { - const mode = new BracketMode(); - const model = createTextModel('{ } { } { }', mode.languageId); - - withTestCodeEditor(model, {}, (editor) => { - const bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController); - - // cursors inside brackets become selections of the entire bracket contents - editor.setSelections([ - new Selection(1, 3, 1, 3), - new Selection(1, 10, 1, 10), - new Selection(1, 17, 1, 17) - ]); - bracketMatchingController.selectToBracket(true); - assert.deepStrictEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 5), - new Selection(1, 8, 1, 13), - new Selection(1, 16, 1, 19) - ]); - - // cursors to the left of bracket pairs become selections of the entire pair - editor.setSelections([ - new Selection(1, 1, 1, 1), - new Selection(1, 6, 1, 6), - new Selection(1, 14, 1, 14) - ]); - bracketMatchingController.selectToBracket(true); - assert.deepStrictEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 5), - new Selection(1, 8, 1, 13), - new Selection(1, 16, 1, 19) - ]); - - // cursors just right of a bracket pair become selections of the entire pair - editor.setSelections([ - new Selection(1, 5, 1, 5), - new Selection(1, 13, 1, 13), - new Selection(1, 19, 1, 19) - ]); - bracketMatchingController.selectToBracket(true); - assert.deepStrictEqual(editor.getSelections(), [ - new Selection(1, 5, 1, 1), - new Selection(1, 13, 1, 8), - new Selection(1, 19, 1, 16) - ]); - - bracketMatchingController.dispose(); - }); - - model.dispose(); - mode.dispose(); + const editor = createCodeEditorWithBrackets('{ } { } { }'); + const bracketMatchingController = disposables.add(editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController)); + + // cursors inside brackets become selections of the entire bracket contents + editor.setSelections([ + new Selection(1, 3, 1, 3), + new Selection(1, 10, 1, 10), + new Selection(1, 17, 1, 17) + ]); + bracketMatchingController.selectToBracket(true); + assert.deepStrictEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 5), + new Selection(1, 8, 1, 13), + new Selection(1, 16, 1, 19) + ]); + + // cursors to the left of bracket pairs become selections of the entire pair + editor.setSelections([ + new Selection(1, 1, 1, 1), + new Selection(1, 6, 1, 6), + new Selection(1, 14, 1, 14) + ]); + bracketMatchingController.selectToBracket(true); + assert.deepStrictEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 5), + new Selection(1, 8, 1, 13), + new Selection(1, 16, 1, 19) + ]); + + // cursors just right of a bracket pair become selections of the entire pair + editor.setSelections([ + new Selection(1, 5, 1, 5), + new Selection(1, 13, 1, 13), + new Selection(1, 19, 1, 19) + ]); + bracketMatchingController.selectToBracket(true); + assert.deepStrictEqual(editor.getSelections(), [ + new Selection(1, 5, 1, 1), + new Selection(1, 13, 1, 8), + new Selection(1, 19, 1, 16) + ]); }); }); diff --git a/src/vs/editor/contrib/caretOperations/test/browser/moveCarretCommand.test.ts b/src/vs/editor/contrib/caretOperations/test/browser/moveCarretCommand.test.ts index 60dad4fbc01..b30aa76ca63 100644 --- a/src/vs/editor/contrib/caretOperations/test/browser/moveCarretCommand.test.ts +++ b/src/vs/editor/contrib/caretOperations/test/browser/moveCarretCommand.test.ts @@ -9,11 +9,11 @@ import { testCommand } from 'vs/editor/test/browser/testCommand'; function testMoveCaretLeftCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, null, selection, (sel) => new MoveCaretCommand(sel, true), expectedLines, expectedSelection); + testCommand(lines, null, selection, (accessor, sel) => new MoveCaretCommand(sel, true), expectedLines, expectedSelection); } function testMoveCaretRightCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, null, selection, (sel) => new MoveCaretCommand(sel, false), expectedLines, expectedSelection); + testCommand(lines, null, selection, (accessor, sel) => new MoveCaretCommand(sel, false), expectedLines, expectedSelection); } suite('Editor Contrib - Move Caret Command', () => { diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts index b3f036f7331..3921bbb6b91 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts @@ -168,7 +168,13 @@ export async function applyCodeAction( await item.resolve(CancellationToken.None); if (item.action.edit) { - await bulkEditService.apply(ResourceEdit.convert(item.action.edit), { editor, label: item.action.title, code: 'undoredo.codeAction' }); + await bulkEditService.apply(ResourceEdit.convert(item.action.edit), { + editor, + label: item.action.title, + quotableLabel: item.action.title, + code: 'undoredo.codeAction', + respectAutoSaveConfig: true + }); } if (item.action.command) { diff --git a/src/vs/editor/contrib/codelens/browser/codelensController.ts b/src/vs/editor/contrib/codelens/browser/codelensController.ts index d2d2eca45e7..061d33b5f8e 100644 --- a/src/vs/editor/contrib/codelens/browser/codelensController.ts +++ b/src/vs/editor/contrib/codelens/browser/codelensController.ts @@ -88,15 +88,15 @@ export class CodeLensContribution implements IEditorContribution { } private _getLayoutInfo() { + const lineHeightFactor = Math.max(1.3, this._editor.getOption(EditorOption.lineHeight) / this._editor.getOption(EditorOption.fontSize)); let fontSize = this._editor.getOption(EditorOption.codeLensFontSize); - let codeLensHeight: number; if (!fontSize || fontSize < 5) { fontSize = (this._editor.getOption(EditorOption.fontSize) * .9) | 0; - codeLensHeight = this._editor.getOption(EditorOption.lineHeight); - } else { - codeLensHeight = (fontSize * Math.max(1.3, this._editor.getOption(EditorOption.lineHeight) / this._editor.getOption(EditorOption.fontSize))) | 0; } - return { codeLensHeight, fontSize }; + return { + fontSize, + codeLensHeight: (fontSize * lineHeightFactor) | 0, + }; } private _updateLensStyle(): void { diff --git a/src/vs/editor/contrib/codelens/browser/codelensWidget.ts b/src/vs/editor/contrib/codelens/browser/codelensWidget.ts index 2eebad09917..3b77e8b3752 100644 --- a/src/vs/editor/contrib/codelens/browser/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/browser/codelensWidget.ts @@ -98,7 +98,7 @@ class CodeLensContentWidget implements IContentWidget { if (lens.command) { const title = renderLabelWithIcons(lens.command.title.trim()); if (lens.command.id) { - children.push(dom.$('a', { id: String(i), title: lens.command.tooltip }, ...title)); + children.push(dom.$('a', { id: String(i), title: lens.command.tooltip, role: 'button' }, ...title)); this._commands.set(String(i), lens.command); } else { children.push(dom.$('span', { title: lens.command.tooltip }, ...title)); diff --git a/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts b/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts index e9c96926006..c1964f40201 100644 --- a/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts +++ b/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts @@ -12,7 +12,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { ILanguageConfigurationService, LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { BlockCommentCommand } from 'vs/editor/contrib/comment/browser/blockCommentCommand'; export interface IInsertionPoint { @@ -284,7 +284,7 @@ export class LineCommentCommand implements ICommand { private _executeBlockComment(model: ITextModel, builder: IEditOperationBuilder, s: Selection): void { model.tokenizeIfCheap(s.startLineNumber); let languageId = model.getLanguageIdAtPosition(s.startLineNumber, 1); - let config = LanguageConfigurationRegistry.getComments(languageId); + const config = this.languageConfigurationService.getLanguageConfiguration(languageId).comments; if (!config || !config.blockCommentStartToken || !config.blockCommentEndToken) { // Mode does not support block comments return; diff --git a/src/vs/editor/contrib/comment/test/browser/blockCommentCommand.test.ts b/src/vs/editor/contrib/comment/test/browser/blockCommentCommand.test.ts index db8f9ccde1d..51e71c6aab3 100644 --- a/src/vs/editor/contrib/comment/test/browser/blockCommentCommand.test.ts +++ b/src/vs/editor/contrib/comment/test/browser/blockCommentCommand.test.ts @@ -2,16 +2,31 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from 'vs/base/common/lifecycle'; import { Selection } from 'vs/editor/common/core/selection'; +import { ICommand } from 'vs/editor/common/editorCommon'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { BlockCommentCommand } from 'vs/editor/contrib/comment/browser/blockCommentCommand'; import { testCommand } from 'vs/editor/test/browser/testCommand'; -import { CommentMode } from 'vs/editor/test/common/commentMode'; -import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; + +function _testCommentCommand(lines: string[], selection: Selection, commandFactory: (accessor: ServicesAccessor, selection: Selection) => ICommand, expectedLines: string[], expectedSelection: Selection): void { + const languageId = 'commentMode'; + const prepare = (accessor: ServicesAccessor, disposables: DisposableStore) => { + const languageConfigurationService = accessor.get(ILanguageConfigurationService); + const languageService = accessor.get(ILanguageService); + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + comments: { lineComment: '!@#', blockComment: ['<0', '0>'] } + })); + }; + testCommand(lines, languageId, selection, commandFactory, expectedLines, expectedSelection, undefined, prepare); +} function testBlockCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - let mode = new CommentMode({ lineComment: '!@#', blockComment: ['<0', '0>'] }); - testCommand(lines, mode.languageId, selection, (sel) => new BlockCommentCommand(sel, true, new TestLanguageConfigurationService()), expectedLines, expectedSelection); - mode.dispose(); + _testCommentCommand(lines, selection, (accessor, sel) => new BlockCommentCommand(sel, true, accessor.get(ILanguageConfigurationService)), expectedLines, expectedSelection); } suite('Editor Contrib - Block Comment Command', () => { @@ -470,14 +485,9 @@ suite('Editor Contrib - Block Comment Command', () => { ); }); - test('', () => { - }); - test('insertSpace false', () => { function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - let mode = new CommentMode({ lineComment: '!@#', blockComment: ['<0', '0>'] }); - testCommand(lines, mode.languageId, selection, (sel) => new BlockCommentCommand(sel, false, new TestLanguageConfigurationService()), expectedLines, expectedSelection); - mode.dispose(); + _testCommentCommand(lines, selection, (accessor, sel) => new BlockCommentCommand(sel, false, accessor.get(ILanguageConfigurationService)), expectedLines, expectedSelection); } testLineCommentCommand( @@ -494,9 +504,7 @@ suite('Editor Contrib - Block Comment Command', () => { test('insertSpace false does not remove space', () => { function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - let mode = new CommentMode({ lineComment: '!@#', blockComment: ['<0', '0>'] }); - testCommand(lines, mode.languageId, selection, (sel) => new BlockCommentCommand(sel, false, new TestLanguageConfigurationService()), expectedLines, expectedSelection); - mode.dispose(); + _testCommentCommand(lines, selection, (accessor, sel) => new BlockCommentCommand(sel, false, accessor.get(ILanguageConfigurationService)), expectedLines, expectedSelection); } testLineCommentCommand( diff --git a/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts b/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts index 4af97443a7e..4bce445798f 100644 --- a/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts +++ b/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts @@ -9,22 +9,27 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ICommand } from 'vs/editor/common/editorCommon'; import { EncodedTokenizationResult, ColorId, IState, MetadataConsts, TokenizationRegistry } from 'vs/editor/common/languages'; import { CommentRule } from 'vs/editor/common/languages/languageConfiguration'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { NullState } from 'vs/editor/common/languages/nullTokenize'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ILinePreflightData, IPreflightData, ISimpleModel, LineCommentCommand, Type } from 'vs/editor/contrib/comment/browser/lineCommentCommand'; import { testCommand } from 'vs/editor/test/browser/testCommand'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { CommentMode } from 'vs/editor/test/common/commentMode'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; -function createTestCommandHelper(commentsConfig: CommentRule, commandFactory: (selection: Selection) => ICommand): (lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection) => void { +function createTestCommandHelper(commentsConfig: CommentRule, commandFactory: (accessor: ServicesAccessor, selection: Selection) => ICommand): (lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection) => void { return (lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection) => { - const setup = (accessor: ServicesAccessor, disposables: DisposableStore) => { - disposables.add(new CommentMode(commentsConfig)); + const languageId = 'commentMode'; + const prepare = (accessor: ServicesAccessor, disposables: DisposableStore) => { + const languageConfigurationService = accessor.get(ILanguageConfigurationService); + const languageService = accessor.get(ILanguageService); + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + comments: commentsConfig + })); }; - testCommand(lines, CommentMode.id, selection, commandFactory, expectedLines, expectedSelection, false, setup); + testCommand(lines, languageId, selection, commandFactory, expectedLines, expectedSelection, false, prepare); }; } @@ -32,12 +37,12 @@ suite('Editor Contrib - Line Comment Command', () => { const testLineCommentCommand = createTestCommandHelper( { lineComment: '!@#', blockComment: ['<!@#', '#@!>'] }, - (sel) => new LineCommentCommand(new TestLanguageConfigurationService(), sel, 4, Type.Toggle, true, true) + (accessor, sel) => new LineCommentCommand(accessor.get(ILanguageConfigurationService), sel, 4, Type.Toggle, true, true) ); const testAddLineCommentCommand = createTestCommandHelper( { lineComment: '!@#', blockComment: ['<!@#', '#@!>'] }, - (sel) => new LineCommentCommand(new TestLanguageConfigurationService(), sel, 4, Type.ForceAdd, true, true) + (accessor, sel) => new LineCommentCommand(accessor.get(ILanguageConfigurationService), sel, 4, Type.ForceAdd, true, true) ); test('comment single line', function () { @@ -58,7 +63,7 @@ suite('Editor Contrib - Line Comment Command', () => { test('case insensitive', function () { const testLineCommentCommand = createTestCommandHelper( { lineComment: 'rem' }, - (sel) => new LineCommentCommand(new TestLanguageConfigurationService(), sel, 4, Type.Toggle, true, true) + (accessor, sel) => new LineCommentCommand(accessor.get(ILanguageConfigurationService), sel, 4, Type.Toggle, true, true) ); testLineCommentCommand( @@ -641,7 +646,7 @@ suite('Editor Contrib - Line Comment Command', () => { test('insertSpace false', () => { const testLineCommentCommand = createTestCommandHelper( { lineComment: '!@#' }, - (sel) => new LineCommentCommand(new TestLanguageConfigurationService(), sel, 4, Type.Toggle, false, true) + (accessor, sel) => new LineCommentCommand(accessor.get(ILanguageConfigurationService), sel, 4, Type.Toggle, false, true) ); testLineCommentCommand( @@ -659,7 +664,7 @@ suite('Editor Contrib - Line Comment Command', () => { test('insertSpace false does not remove space', () => { const testLineCommentCommand = createTestCommandHelper( { lineComment: '!@#' }, - (sel) => new LineCommentCommand(new TestLanguageConfigurationService(), sel, 4, Type.Toggle, false, true) + (accessor, sel) => new LineCommentCommand(accessor.get(ILanguageConfigurationService), sel, 4, Type.Toggle, false, true) ); testLineCommentCommand( @@ -678,7 +683,7 @@ suite('Editor Contrib - Line Comment Command', () => { const testLineCommentCommand = createTestCommandHelper( { lineComment: '!@#', blockComment: ['<!@#', '#@!>'] }, - (sel) => new LineCommentCommand(new TestLanguageConfigurationService(), sel, 4, Type.Toggle, true, false) + (accessor, sel) => new LineCommentCommand(accessor.get(ILanguageConfigurationService), sel, 4, Type.Toggle, true, false) ); test('does not ignore whitespace lines', () => { @@ -770,7 +775,7 @@ suite('Editor Contrib - Line Comment As Block Comment', () => { const testLineCommentCommand = createTestCommandHelper( { lineComment: '', blockComment: ['(', ')'] }, - (sel) => new LineCommentCommand(new TestLanguageConfigurationService(), sel, 4, Type.Toggle, true, true) + (accessor, sel) => new LineCommentCommand(accessor.get(ILanguageConfigurationService), sel, 4, Type.Toggle, true, true) ); test('fall back to block comment command', function () { @@ -881,7 +886,7 @@ suite('Editor Contrib - Line Comment As Block Comment 2', () => { const testLineCommentCommand = createTestCommandHelper( { lineComment: null, blockComment: ['<!@#', '#@!>'] }, - (sel) => new LineCommentCommand(new TestLanguageConfigurationService(), sel, 4, Type.Toggle, true, true) + (accessor, sel) => new LineCommentCommand(accessor.get(ILanguageConfigurationService), sel, 4, Type.Toggle, true, true) ); test('no selection => uses indentation', function () { @@ -1081,10 +1086,11 @@ suite('Editor Contrib - Line Comment in mixed modes', () => { class OuterMode extends MockMode { constructor( commentsConfig: CommentRule, - @ILanguageService languageService: ILanguageService + @ILanguageService languageService: ILanguageService, + @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService ) { super(OUTER_LANGUAGE_ID); - this._register(LanguageConfigurationRegistry.register(this.languageId, { + this._register(languageConfigurationService.register(this.languageId, { comments: commentsConfig })); @@ -1110,9 +1116,12 @@ suite('Editor Contrib - Line Comment in mixed modes', () => { } class InnerMode extends MockMode { - constructor(commentsConfig: CommentRule) { + constructor( + commentsConfig: CommentRule, + @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService + ) { super(INNER_LANGUAGE_ID); - this._register(LanguageConfigurationRegistry.register(this.languageId, { + this._register(languageConfigurationService.register(this.languageId, { comments: commentsConfig })); } @@ -1130,7 +1139,7 @@ suite('Editor Contrib - Line Comment in mixed modes', () => { lines, OUTER_LANGUAGE_ID, selection, - (sel) => new LineCommentCommand(new TestLanguageConfigurationService(), sel, 4, Type.Toggle, true, true), + (accessor, sel) => new LineCommentCommand(accessor.get(ILanguageConfigurationService), sel, 4, Type.Toggle, true, true), expectedLines, expectedSelection, true, diff --git a/src/vs/editor/contrib/find/browser/findWidget.css b/src/vs/editor/contrib/find/browser/findWidget.css index 126110305bb..2a7478d362e 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.css +++ b/src/vs/editor/contrib/find/browser/findWidget.css @@ -166,7 +166,7 @@ } .monaco-editor .find-widget .disabled { - opacity: 0.3; + color: var(--vscode-disabledForeground); cursor: default; } diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index 5efbf7963dc..bf6aaa8d8bd 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -472,7 +472,7 @@ export class FoldingController extends Disposable implements IEditorContribution } private onEditorMouseUp(e: IEditorMouseEvent): void { - const foldingModel = this.getFoldingModel(); + const foldingModel = this.foldingModel; if (!foldingModel || !this.mouseDownInfo || !e.target) { return; } @@ -495,47 +495,43 @@ export class FoldingController extends Disposable implements IEditorContribution } } - foldingModel.then(foldingModel => { - if (foldingModel) { - let region = foldingModel.getRegionAtLine(lineNumber); - if (region && region.startLineNumber === lineNumber) { - let isCollapsed = region.isCollapsed; - if (iconClicked || isCollapsed) { - let surrounding = e.event.altKey; - let toToggle = []; - if (surrounding) { - let filter = (otherRegion: FoldingRegion) => !otherRegion.containedBy(region!) && !region!.containedBy(otherRegion); - let toMaybeToggle = foldingModel.getRegionsInside(null, filter); - for (const r of toMaybeToggle) { - if (r.isCollapsed) { - toToggle.push(r); - } - } - // if any surrounding regions are folded, unfold those. Otherwise, fold all surrounding - if (toToggle.length === 0) { - toToggle = toMaybeToggle; - } + let region = foldingModel.getRegionAtLine(lineNumber); + if (region && region.startLineNumber === lineNumber) { + let isCollapsed = region.isCollapsed; + if (iconClicked || isCollapsed) { + let surrounding = e.event.altKey; + let toToggle = []; + if (surrounding) { + let filter = (otherRegion: FoldingRegion) => !otherRegion.containedBy(region!) && !region!.containedBy(otherRegion); + let toMaybeToggle = foldingModel.getRegionsInside(null, filter); + for (const r of toMaybeToggle) { + if (r.isCollapsed) { + toToggle.push(r); } - else { - let recursive = e.event.middleButton || e.event.shiftKey; - if (recursive) { - for (const r of foldingModel.getRegionsInside(region)) { - if (r.isCollapsed === isCollapsed) { - toToggle.push(r); - } - } - } - // when recursive, first only collapse all children. If all are already folded or there are no children, also fold parent. - if (isCollapsed || !recursive || toToggle.length === 0) { - toToggle.push(region); + } + // if any surrounding regions are folded, unfold those. Otherwise, fold all surrounding + if (toToggle.length === 0) { + toToggle = toMaybeToggle; + } + } + else { + let recursive = e.event.middleButton || e.event.shiftKey; + if (recursive) { + for (const r of foldingModel.getRegionsInside(region)) { + if (r.isCollapsed === isCollapsed) { + toToggle.push(r); } } - foldingModel.toggleCollapseState(toToggle); - this.reveal({ lineNumber, column: 1 }); + } + // when recursive, first only collapse all children. If all are already folded or there are no children, also fold parent. + if (isCollapsed || !recursive || toToggle.length === 0) { + toToggle.push(region); } } + foldingModel.toggleCollapseState(toToggle); + this.reveal({ lineNumber, column: 1 }); } - }).then(undefined, onUnexpectedError); + } } public reveal(position: IPosition): void { diff --git a/src/vs/editor/contrib/indentation/browser/indentation.ts b/src/vs/editor/contrib/indentation/browser/indentation.ts index 751ec8e5051..c7ea4ee4b3a 100644 --- a/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -16,13 +16,14 @@ import { ICommand, ICursorStateComputerData, IEditOperationBuilder, IEditorContr import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { EndOfLineSequence, ITextModel } from 'vs/editor/common/model'; import { StandardTokenType, TextEdit } from 'vs/editor/common/languages'; -import { ILanguageConfigurationService, LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IndentConsts } from 'vs/editor/common/languages/supports/indentRules'; import { IModelService } from 'vs/editor/common/services/model'; import * as indentUtils from 'vs/editor/contrib/indentation/browser/indentUtils'; import * as nls from 'vs/nls'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { normalizeIndentation } from 'vs/editor/common/core/indentation'; +import { getGoodIndentForLine, getIndentMetadata } from 'vs/editor/common/languages/autoIndent'; export function getReindentEditOperations(model: ITextModel, languageConfigurationService: ILanguageConfigurationService, startLineNumber: number, endLineNumber: number, inheritedIndent?: string): ISingleEditOperation[] { if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) { @@ -428,12 +429,13 @@ export class AutoIndentOnPasteCommand implements ICommand { export class AutoIndentOnPaste implements IEditorContribution { public static readonly ID = 'editor.contrib.autoIndentOnPaste'; - private readonly editor: ICodeEditor; private readonly callOnDispose = new DisposableStore(); private readonly callOnModel = new DisposableStore(); - constructor(editor: ICodeEditor) { - this.editor = editor; + constructor( + private readonly editor: ICodeEditor, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService + ) { this.callOnDispose.add(editor.onDidChangeConfiguration(() => this.update())); this.callOnDispose.add(editor.onDidChangeModel(() => this.update())); @@ -503,7 +505,7 @@ export class AutoIndentOnPaste implements IEditorContribution { let firstLineText = model.getLineContent(startLineNumber); if (!/\S/.test(firstLineText.substring(0, range.startColumn - 1))) { - const indentOfFirstLine = LanguageConfigurationRegistry.getGoodIndentForLine(autoIndent, model, model.getLanguageId(), startLineNumber, indentConverter); + const indentOfFirstLine = getGoodIndentForLine(autoIndent, model, model.getLanguageId(), startLineNumber, indentConverter, this._languageConfigurationService); if (indentOfFirstLine !== null) { let oldIndentation = strings.getLeadingWhitespace(firstLineText); @@ -518,7 +520,7 @@ export class AutoIndentOnPaste implements IEditorContribution { }); firstLineText = newIndent + firstLineText.substr(oldIndentation.length); } else { - let indentMetadata = LanguageConfigurationRegistry.getIndentMetadata(model, startLineNumber); + let indentMetadata = getIndentMetadata(model, startLineNumber, this._languageConfigurationService); if (indentMetadata === 0 || indentMetadata === IndentConsts.UNINDENT_MASK) { // we paste content into a line where only contains whitespaces @@ -561,7 +563,7 @@ export class AutoIndentOnPaste implements IEditorContribution { } } }; - let indentOfSecondLine = LanguageConfigurationRegistry.getGoodIndentForLine(autoIndent, virtualModel, model.getLanguageId(), startLineNumber + 1, indentConverter); + let indentOfSecondLine = getGoodIndentForLine(autoIndent, virtualModel, model.getLanguageId(), startLineNumber + 1, indentConverter, this._languageConfigurationService); if (indentOfSecondLine !== null) { let newSpaceCntOfSecondLine = indentUtils.getSpaceCnt(indentOfSecondLine, tabSize); let oldSpaceCntOfSecondLine = indentUtils.getSpaceCnt(strings.getLeadingWhitespace(model.getLineContent(startLineNumber + 1)), tabSize); diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 3aeed9aba70..052ffdb67af 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -8,11 +8,11 @@ import { IndentationToSpacesCommand, IndentationToTabsCommand } from 'vs/editor/ import { testCommand } from 'vs/editor/test/browser/testCommand'; function testIndentationToSpacesCommand(lines: string[], selection: Selection, tabSize: number, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, null, selection, (sel) => new IndentationToSpacesCommand(sel, tabSize), expectedLines, expectedSelection); + testCommand(lines, null, selection, (accessor, sel) => new IndentationToSpacesCommand(sel, tabSize), expectedLines, expectedSelection); } function testIndentationToTabsCommand(lines: string[], selection: Selection, tabSize: number, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, null, selection, (sel) => new IndentationToTabsCommand(sel, tabSize), expectedLines, expectedSelection); + testCommand(lines, null, selection, (accessor, sel) => new IndentationToTabsCommand(sel, tabSize), expectedLines, expectedSelection); } suite('Editor Contrib - Indentation to Spaces', () => { diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts index 89a5965e86b..a1974f92176 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts @@ -409,7 +409,7 @@ export class InlayHintsController implements IEditorContribution { // - const { fontSize, fontFamily } = this._getLayoutInfo(); + const { fontSize, fontFamily, displayStyle } = this._getLayoutInfo(); const fontFamilyVar = '--code-editorInlayHintsFontFamily'; this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily); @@ -434,7 +434,6 @@ export class InlayHintsController implements IEditorContribution { const cssProperties: CssProperties = { fontSize: `${fontSize}px`, fontFamily: `var(${fontFamilyVar}), ${EDITOR_FONT_DEFAULTS.fontFamily}`, - verticalAlign: 'middle', }; if (isNonEmptyArray(item.hint.textEdits)) { @@ -452,20 +451,24 @@ export class InlayHintsController implements IEditorContribution { } } - if (isFirst && isLast) { - // only element - cssProperties.padding = `1px ${Math.max(1, fontSize / 4) | 0}px`; - cssProperties.borderRadius = `${(fontSize / 4) | 0}px`; - } else if (isFirst) { - // first element - cssProperties.padding = `1px 0 1px ${Math.max(1, fontSize / 4) | 0}px`; - cssProperties.borderRadius = `${(fontSize / 4) | 0}px 0 0 ${(fontSize / 4) | 0}px`; - } else if (isLast) { - // last element - cssProperties.padding = `1px ${Math.max(1, fontSize / 4) | 0}px 1px 0`; - cssProperties.borderRadius = `0 ${(fontSize / 4) | 0}px ${(fontSize / 4) | 0}px 0`; - } else { - cssProperties.padding = `1px 0 1px 0`; + if (displayStyle === 'standard') { + cssProperties.verticalAlign = 'middle'; + + if (isFirst && isLast) { + // only element + cssProperties.padding = `1px ${Math.max(1, fontSize / 4) | 0}px`; + cssProperties.borderRadius = `${(fontSize / 4) | 0}px`; + } else if (isFirst) { + // first element + cssProperties.padding = `1px 0 1px ${Math.max(1, fontSize / 4) | 0}px`; + cssProperties.borderRadius = `${(fontSize / 4) | 0}px 0 0 ${(fontSize / 4) | 0}px`; + } else if (isLast) { + // last element + cssProperties.padding = `1px ${Math.max(1, fontSize / 4) | 0}px 1px 0`; + cssProperties.borderRadius = `0 ${(fontSize / 4) | 0}px ${(fontSize / 4) | 0}px 0`; + } else { + cssProperties.padding = `1px 0 1px 0`; + } } addInjectedText( @@ -526,10 +529,10 @@ export class InlayHintsController implements IEditorContribution { const editorFontSize = this._editor.getOption(EditorOption.fontSize); let fontSize = options.fontSize; if (!fontSize || fontSize < 5 || fontSize > editorFontSize) { - fontSize = (editorFontSize * .9) | 0; + fontSize = editorFontSize; } const fontFamily = options.fontFamily || this._editor.getOption(EditorOption.fontFamily); - return { fontSize, fontFamily }; + return { fontSize, fontFamily, displayStyle: options.displayStyle }; } private _removeAllDecorations(): void { diff --git a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts index 52993bdb46a..e86aaad67fc 100644 --- a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts @@ -24,6 +24,7 @@ import { SortLinesCommand } from 'vs/editor/contrib/linesOperations/browser/sort import * as nls from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; // copy lines @@ -170,14 +171,15 @@ abstract class AbstractMoveLinesAction extends EditorAction { this.down = down; } - public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + const languageConfigurationService = accessor.get(ILanguageConfigurationService); let commands: ICommand[] = []; let selections = editor.getSelections() || []; const autoIndent = editor.getOption(EditorOption.autoIndent); for (const selection of selections) { - commands.push(new MoveLinesCommand(selection, this.down, autoIndent)); + commands.push(new MoveLinesCommand(selection, this.down, autoIndent, languageConfigurationService)); } editor.pushUndoStop(); diff --git a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts index c3ede1e5e0f..81d40b01d43 100644 --- a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts @@ -11,9 +11,11 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { CompleteEnterAction, IndentAction } from 'vs/editor/common/languages/languageConfiguration'; -import { IIndentConverter, LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +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 { getEnterAction } from 'vs/editor/common/languages/enterAction'; export class MoveLinesCommand implements ICommand { @@ -25,7 +27,12 @@ export class MoveLinesCommand implements ICommand { private _moveEndPositionDown?: boolean; private _moveEndLineSelectionShrink: boolean; - constructor(selection: Selection, isMovingDown: boolean, autoIndent: EditorAutoIndentStrategy) { + constructor( + selection: Selection, + isMovingDown: boolean, + autoIndent: EditorAutoIndentStrategy, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService + ) { this._selection = selection; this._isMovingDown = isMovingDown; this._autoIndent = autoIndent; @@ -118,8 +125,14 @@ export class MoveLinesCommand implements ICommand { return model.getLineContent(lineNumber); } }; - let indentOfMovingLine = LanguageConfigurationRegistry.getGoodIndentForLine(this._autoIndent, virtualModel, model.getLanguageIdAtPosition( - movingLineNumber, 1), s.startLineNumber, indentConverter); + let indentOfMovingLine = getGoodIndentForLine( + this._autoIndent, + virtualModel, + model.getLanguageIdAtPosition(movingLineNumber, 1), + s.startLineNumber, + indentConverter, + this._languageConfigurationService + ); if (indentOfMovingLine !== null) { let oldIndentation = strings.getLeadingWhitespace(model.getLineContent(movingLineNumber)); let newSpaceCnt = indentUtils.getSpaceCnt(indentOfMovingLine, tabSize); @@ -154,8 +167,14 @@ export class MoveLinesCommand implements ICommand { } }; - let newIndentatOfMovingBlock = LanguageConfigurationRegistry.getGoodIndentForLine(this._autoIndent, virtualModel, model.getLanguageIdAtPosition( - movingLineNumber, 1), s.startLineNumber + 1, indentConverter); + let newIndentatOfMovingBlock = getGoodIndentForLine( + this._autoIndent, + virtualModel, + model.getLanguageIdAtPosition(movingLineNumber, 1), + s.startLineNumber + 1, + indentConverter, + this._languageConfigurationService + ); if (newIndentatOfMovingBlock !== null) { const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(s.startLineNumber)); @@ -199,7 +218,14 @@ export class MoveLinesCommand implements ICommand { } } else { // it doesn't match any onEnter rule, let's check indentation rules then. - let indentOfFirstLine = LanguageConfigurationRegistry.getGoodIndentForLine(this._autoIndent, virtualModel, model.getLanguageIdAtPosition(s.startLineNumber, 1), movingLineNumber, indentConverter); + let indentOfFirstLine = getGoodIndentForLine( + this._autoIndent, + virtualModel, + model.getLanguageIdAtPosition(s.startLineNumber, 1), + movingLineNumber, + indentConverter, + this._languageConfigurationService + ); if (indentOfFirstLine !== null) { // adjust the indentation of the moving block let oldIndent = strings.getLeadingWhitespace(model.getLineContent(s.startLineNumber)); @@ -247,7 +273,7 @@ export class MoveLinesCommand implements ICommand { if (this.trimLeft(movingLineText).indexOf(this.trimLeft(enterPrefix)) >= 0) { let oldIndentation = strings.getLeadingWhitespace(model.getLineContent(line)); let newIndentation = strings.getLeadingWhitespace(enterPrefix); - let indentMetadataOfMovelingLine = LanguageConfigurationRegistry.getIndentMetadata(model, line); + let indentMetadataOfMovelingLine = getIndentMetadata(model, line, this._languageConfigurationService); if (indentMetadataOfMovelingLine !== null && indentMetadataOfMovelingLine & IndentConsts.DECREASE_MASK) { newIndentation = indentConverter.unshiftIndent(newIndentation); } @@ -273,7 +299,7 @@ export class MoveLinesCommand implements ICommand { if (strings.lastNonWhitespaceIndex(futureAboveLineText) >= 0) { // break let maxColumn = model.getLineMaxColumn(futureAboveLineNumber); - let enter = LanguageConfigurationRegistry.getEnterAction(this._autoIndent, model, new Range(futureAboveLineNumber, maxColumn, futureAboveLineNumber, maxColumn)); + let enter = getEnterAction(this._autoIndent, model, new Range(futureAboveLineNumber, maxColumn, futureAboveLineNumber, maxColumn), this._languageConfigurationService); return this.parseEnterResult(model, indentConverter, tabSize, line, enter); } else { // go upwards, starting from `line - 1` @@ -294,7 +320,7 @@ export class MoveLinesCommand implements ICommand { } let maxColumn = model.getLineMaxColumn(validPrecedingLine); - let enter = LanguageConfigurationRegistry.getEnterAction(this._autoIndent, model, new Range(validPrecedingLine, maxColumn, validPrecedingLine, maxColumn)); + let enter = getEnterAction(this._autoIndent, model, new Range(validPrecedingLine, maxColumn, validPrecedingLine, maxColumn), this._languageConfigurationService); return this.parseEnterResult(model, indentConverter, tabSize, line, enter); } } @@ -322,7 +348,7 @@ export class MoveLinesCommand implements ICommand { } let maxColumn = model.getLineMaxColumn(validPrecedingLine); - let enter = LanguageConfigurationRegistry.getEnterAction(this._autoIndent, model, new Range(validPrecedingLine, maxColumn, validPrecedingLine, maxColumn)); + let enter = getEnterAction(this._autoIndent, model, new Range(validPrecedingLine, maxColumn, validPrecedingLine, maxColumn), this._languageConfigurationService); return this.parseEnterResult(model, indentConverter, tabSize, line, enter); } @@ -345,7 +371,7 @@ export class MoveLinesCommand implements ICommand { return false; } - if (LanguageConfigurationRegistry.getIndentRulesSupport(languageAtSelectionStart) === null) { + if (this._languageConfigurationService.getLanguageConfiguration(languageAtSelectionStart).indentRulesSupport === null) { return false; } diff --git a/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts index 9881cb0e4ed..16cc2c305f0 100644 --- a/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/browser/copyLinesCommand.test.ts @@ -11,11 +11,11 @@ import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { testCommand } from 'vs/editor/test/browser/testCommand'; function testCopyLinesDownCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, null, selection, (sel) => new CopyLinesCommand(sel, true), expectedLines, expectedSelection); + testCommand(lines, null, selection, (accessor, sel) => new CopyLinesCommand(sel, true), expectedLines, expectedSelection); } function testCopyLinesUpCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, null, selection, (sel) => new CopyLinesCommand(sel, false), expectedLines, expectedSelection); + testCommand(lines, null, selection, (accessor, sel) => new CopyLinesCommand(sel, false), expectedLines, expectedSelection); } suite('Editor Contrib - Copy Lines Command', () => { diff --git a/src/vs/editor/contrib/linesOperations/test/browser/moveLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/browser/moveLinesCommand.test.ts index a7735a08dbc..6ad8ce59833 100644 --- a/src/vs/editor/contrib/linesOperations/test/browser/moveLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/browser/moveLinesCommand.test.ts @@ -5,25 +5,26 @@ import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; import { Selection } from 'vs/editor/common/core/selection'; import { IndentationRule } from 'vs/editor/common/languages/languageConfiguration'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { MoveLinesCommand } from 'vs/editor/contrib/linesOperations/browser/moveLinesCommand'; import { testCommand } from 'vs/editor/test/browser/testCommand'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; -function testMoveLinesDownCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, null, selection, (sel) => new MoveLinesCommand(sel, true, EditorAutoIndentStrategy.Advanced), expectedLines, expectedSelection); +function testMoveLinesDownCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService = new TestLanguageConfigurationService()): void { + testCommand(lines, null, selection, (accessor, sel) => new MoveLinesCommand(sel, true, EditorAutoIndentStrategy.Advanced, languageConfigurationService), expectedLines, expectedSelection); } -function testMoveLinesUpCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, null, selection, (sel) => new MoveLinesCommand(sel, false, EditorAutoIndentStrategy.Advanced), expectedLines, expectedSelection); +function testMoveLinesUpCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService = new TestLanguageConfigurationService()): void { + testCommand(lines, null, selection, (accessor, sel) => new MoveLinesCommand(sel, false, EditorAutoIndentStrategy.Advanced, languageConfigurationService), expectedLines, expectedSelection); } -function testMoveLinesDownWithIndentCommand(languageId: string, lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, languageId, selection, (sel) => new MoveLinesCommand(sel, true, EditorAutoIndentStrategy.Full), expectedLines, expectedSelection); +function testMoveLinesDownWithIndentCommand(languageId: string, lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService = new TestLanguageConfigurationService()): void { + testCommand(lines, languageId, selection, (accessor, sel) => new MoveLinesCommand(sel, true, EditorAutoIndentStrategy.Full, languageConfigurationService), expectedLines, expectedSelection); } -function testMoveLinesUpWithIndentCommand(languageId: string, lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, languageId, selection, (sel) => new MoveLinesCommand(sel, false, EditorAutoIndentStrategy.Full), expectedLines, expectedSelection); +function testMoveLinesUpWithIndentCommand(languageId: string, lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService = new TestLanguageConfigurationService()): void { + testCommand(lines, languageId, selection, (accessor, sel) => new MoveLinesCommand(sel, false, EditorAutoIndentStrategy.Full, languageConfigurationService), expectedLines, expectedSelection); } suite('Editor Contrib - Move Lines Command', () => { @@ -260,9 +261,12 @@ suite('Editor Contrib - Move Lines Command', () => { class IndentRulesMode extends MockMode { private static readonly _id = 'moveLinesIndentMode'; - constructor(indentationRules: IndentationRule) { + constructor( + indentationRules: IndentationRule, + @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService + ) { super(IndentRulesMode._id); - this._register(LanguageConfigurationRegistry.register(this.languageId, { + this._register(languageConfigurationService.register(this.languageId, { indentationRules: indentationRules })); } @@ -278,7 +282,8 @@ suite('Editor contrib - Move Lines Command honors Indentation Rules', () => { // https://github.com/microsoft/vscode/issues/28552#issuecomment-307862797 test('first line indentation adjust to 0', () => { - let mode = new IndentRulesMode(indentRules); + const languageConfigurationService = new TestLanguageConfigurationService(); + const mode = new IndentRulesMode(indentRules, languageConfigurationService); testMoveLinesUpWithIndentCommand( mode.languageId, @@ -293,7 +298,8 @@ suite('Editor contrib - Move Lines Command honors Indentation Rules', () => { 'class X {', '}' ], - new Selection(1, 1, 1, 1) + new Selection(1, 1, 1, 1), + languageConfigurationService ); mode.dispose(); @@ -301,7 +307,8 @@ suite('Editor contrib - Move Lines Command honors Indentation Rules', () => { // https://github.com/microsoft/vscode/issues/28552#issuecomment-307867717 test('move lines across block', () => { - let mode = new IndentRulesMode(indentRules); + const languageConfigurationService = new TestLanguageConfigurationService(); + const mode = new IndentRulesMode(indentRules, languageConfigurationService); testMoveLinesDownWithIndentCommand( mode.languageId, @@ -322,7 +329,8 @@ suite('Editor contrib - Move Lines Command honors Indentation Rules', () => { ' }', '];' ], - new Selection(2, 5, 2, 5) + new Selection(2, 5, 2, 5), + languageConfigurationService ); mode.dispose(); @@ -354,9 +362,11 @@ suite('Editor contrib - Move Lines Command honors Indentation Rules', () => { class EnterRulesMode extends MockMode { private static readonly _id = 'moveLinesEnterMode'; - constructor() { + constructor( + @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService + ) { super(EnterRulesMode._id); - this._register(LanguageConfigurationRegistry.register(this.languageId, { + this._register(languageConfigurationService.register(this.languageId, { indentationRules: { decreaseIndentPattern: /^\s*\[$/, increaseIndentPattern: /^\s*\]$/, @@ -371,7 +381,8 @@ class EnterRulesMode extends MockMode { suite('Editor - contrib - Move Lines Command honors onEnter Rules', () => { test('issue #54829. move block across block', () => { - let mode = new EnterRulesMode(); + const languageConfigurationService = new TestLanguageConfigurationService(); + const mode = new EnterRulesMode(languageConfigurationService); testMoveLinesDownWithIndentCommand( mode.languageId, @@ -398,6 +409,7 @@ suite('Editor - contrib - Move Lines Command honors onEnter Rules', () => { '}' ], new Selection(4, 9, 6, 10), + languageConfigurationService ); mode.dispose(); diff --git a/src/vs/editor/contrib/linesOperations/test/browser/sortLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/browser/sortLinesCommand.test.ts index 62f60269b52..630d2bdb2f1 100644 --- a/src/vs/editor/contrib/linesOperations/test/browser/sortLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/browser/sortLinesCommand.test.ts @@ -8,11 +8,11 @@ import { SortLinesCommand } from 'vs/editor/contrib/linesOperations/browser/sort import { testCommand } from 'vs/editor/test/browser/testCommand'; function testSortLinesAscendingCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, null, selection, (sel) => new SortLinesCommand(sel, false), expectedLines, expectedSelection); + testCommand(lines, null, selection, (accessor, sel) => new SortLinesCommand(sel, false), expectedLines, expectedSelection); } function testSortLinesDescendingCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, null, selection, (sel) => new SortLinesCommand(sel, true), expectedLines, expectedSelection); + testCommand(lines, null, selection, (accessor, sel) => new SortLinesCommand(sel, true), expectedLines, expectedSelection); } suite('Editor Contrib - Sort Lines Command', () => { 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 a66491e7c89..93d7ee89440 100644 --- a/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts +++ b/src/vs/editor/contrib/linkedEditing/test/browser/linkedEditing.test.ts @@ -12,13 +12,15 @@ import { IRange, Range } from 'vs/editor/common/core/range'; import { Handler } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/core/wordHelper'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { LinkedEditingContribution } from 'vs/editor/contrib/linkedEditing/browser/linkedEditing'; -import { createTestCodeEditor, ITestCodeEditor, TestCodeEditorInstantiationOptions } from 'vs/editor/test/browser/testCodeEditor'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; -import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { createCodeEditorServices, instantiateTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { instantiateTextModel } from 'vs/editor/test/common/testTextModel'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { DeleteWordLeft } from 'vs/editor/contrib/wordOperations/browser/wordOperations'; +import { DeleteAllLeftAction } from 'vs/editor/contrib/linesOperations/browser/linesOperations'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; const mockFile = URI.parse('test:somefile.ttt'); const mockFileSelector = { scheme: 'test' }; @@ -35,30 +37,32 @@ interface TestEditor { const languageId = 'linkedEditingTestLangage'; suite('linked editing', () => { - const disposables = new DisposableStore(); + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let languageFeaturesService: ILanguageFeaturesService; + let languageConfigurationService: ILanguageConfigurationService; setup(() => { - disposables.clear(); - disposables.add(LanguageConfigurationRegistry.register(languageId, { + disposables = new DisposableStore(); + instantiationService = createCodeEditorServices(disposables); + languageFeaturesService = instantiationService.get(ILanguageFeaturesService); + languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + + disposables.add(languageConfigurationService.register(languageId, { wordPattern: /[a-zA-Z]+/ })); }); teardown(() => { - disposables.clear(); + disposables.dispose(); }); - function createMockEditor(text: string | string[], options: TestCodeEditorInstantiationOptions = {}): ITestCodeEditor { - const model = createTextModel(typeof text === 'string' ? text : text.join('\n'), languageId, undefined, mockFile); - - const editor = createTestCodeEditor(model, options); - disposables.add(model); - disposables.add(editor); - + function createMockEditor(text: string | string[]): ITestCodeEditor { + const model = disposables.add(instantiateTextModel(instantiationService, typeof text === 'string' ? text : text.join('\n'), languageId, undefined, mockFile)); + const editor = disposables.add(instantiateTestCodeEditor(instantiationService, model)); return editor; } - function testCase( name: string, initialState: { text: string | string[]; responseWordPattern?: RegExp }, @@ -66,61 +70,70 @@ suite('linked editing', () => { expectedEndText: string | string[] ) { test(name, async () => { - - const languageFeaturesService = new LanguageFeaturesService(); - const serviceCollection = new ServiceCollection([ILanguageFeaturesService, languageFeaturesService]); - - disposables.add(languageFeaturesService.linkedEditingRangeProvider.register(mockFileSelector, { - provideLinkedEditingRanges(model: ITextModel, pos: IPosition) { - const wordAtPos = model.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 }; + await runWithFakedTimers({}, async () => { + + disposables.add(languageFeaturesService.linkedEditingRangeProvider.register(mockFileSelector, { + provideLinkedEditingRanges(model: ITextModel, pos: IPosition) { + const wordAtPos = model.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 }; + } + return { ranges: [], wordPattern: initialState.responseWordPattern }; } - return { ranges: [], wordPattern: initialState.responseWordPattern }; - } - })); - - const editor = createMockEditor(initialState.text, { serviceCollection }); - editor.updateOptions({ linkedEditing: true }); - const linkedEditingContribution = editor.registerAndInstantiateContribution( - LinkedEditingContribution.ID, - LinkedEditingContribution, - ); - linkedEditingContribution.setDebounceDuration(0); - - const testEditor: TestEditor = { - setPosition(pos: Position) { - editor.setPosition(pos); - return linkedEditingContribution.currentUpdateTriggerPromise; - }, - setSelection(sel: IRange) { - editor.setSelection(sel); - return linkedEditingContribution.currentUpdateTriggerPromise; - }, - trigger(source: string | null | undefined, handlerId: string, payload: any) { - editor.trigger(source, handlerId, payload); - return linkedEditingContribution.currentSyncTriggerPromise; - }, - undo() { - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - }, - redo() { - CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - } - }; - - await operations(testEditor); - - return new Promise<void>((resolve) => { - setTimeout(() => { - if (typeof expectedEndText === 'string') { - assert.strictEqual(editor.getModel()!.getValue(), expectedEndText); - } else { - assert.strictEqual(editor.getModel()!.getValue(), expectedEndText.join('\n')); + })); + + const editor = createMockEditor(initialState.text); + editor.updateOptions({ linkedEditing: true }); + const linkedEditingContribution = disposables.add(editor.registerAndInstantiateContribution( + LinkedEditingContribution.ID, + LinkedEditingContribution, + )); + linkedEditingContribution.setDebounceDuration(0); + + const testEditor: TestEditor = { + setPosition(pos: Position) { + editor.setPosition(pos); + return linkedEditingContribution.currentUpdateTriggerPromise; + }, + setSelection(sel: IRange) { + editor.setSelection(sel); + return linkedEditingContribution.currentUpdateTriggerPromise; + }, + trigger(source: string | null | undefined, handlerId: string, payload: any) { + if (handlerId === Handler.Type || handlerId === Handler.Paste) { + editor.trigger(source, handlerId, payload); + } else if (handlerId === 'deleteLeft') { + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, payload); + } else if (handlerId === 'deleteWordLeft') { + instantiationService.invokeFunction((accessor) => (new DeleteWordLeft()).runEditorCommand(accessor, editor, payload)); + } else if (handlerId === 'deleteAllLeft') { + instantiationService.invokeFunction((accessor) => (new DeleteAllLeftAction()).runEditorCommand(accessor, editor, payload)); + } else { + throw new Error(`Unknown handler ${handlerId}!`); + } + return linkedEditingContribution.currentSyncTriggerPromise; + }, + undo() { + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + }, + redo() { + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); } - resolve(); - }, timeout); + }; + + await operations(testEditor); + + return new Promise<void>((resolve) => { + setTimeout(() => { + if (typeof expectedEndText === 'string') { + assert.strictEqual(editor.getModel()!.getValue(), expectedEndText); + } else { + assert.strictEqual(editor.getModel()!.getValue(), expectedEndText.join('\n')); + } + resolve(); + }, timeout); + }); }); }); } diff --git a/src/vs/editor/contrib/multicursor/browser/multicursor.ts b/src/vs/editor/contrib/multicursor/browser/multicursor.ts index 36dc4294f15..574a5be3e99 100644 --- a/src/vs/editor/contrib/multicursor/browser/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/browser/multicursor.ts @@ -1091,6 +1091,84 @@ function getValueInRange(model: ITextModel, range: Range, toLowerCase: boolean): return (toLowerCase ? text.toLowerCase() : text); } +export class FocusNextCursor extends EditorAction { + constructor() { + super({ + id: 'editor.action.focusNextCursor', + label: nls.localize('mutlicursor.focusNextCursor', "Focus next cursor"), + description: { + description: nls.localize('mutlicursor.focusNextCursor.description', "Focuses the next cursor"), + args: [], + }, + alias: 'Focus Next Cursor', + precondition: undefined + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { + if (!editor.hasModel()) { + return; + } + + const viewModel = editor._getViewModel(); + + if (viewModel.cursorConfig.readOnly) { + return; + } + + viewModel.model.pushStackElement(); + const previousCursorState = Array.from(viewModel.getCursorStates()); + const firstCursor = previousCursorState.shift(); + if (!firstCursor) { + return; + } + previousCursorState.push(firstCursor); + + viewModel.setCursorStates(args.source, CursorChangeReason.Explicit, previousCursorState); + viewModel.revealPrimaryCursor(args.source, true); + announceCursorChange(previousCursorState, viewModel.getCursorStates()); + } +} + +export class FocusPreviousCursor extends EditorAction { + constructor() { + super({ + id: 'editor.action.focusPreviousCursor', + label: nls.localize('mutlicursor.focusPreviousCursor', "Focus previous cursor"), + description: { + description: nls.localize('mutlicursor.focusPreviousCursor.description', "Focuses the previous cursor"), + args: [], + }, + alias: 'Focus Previous Cursor', + precondition: undefined + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { + if (!editor.hasModel()) { + return; + } + + const viewModel = editor._getViewModel(); + + if (viewModel.cursorConfig.readOnly) { + return; + } + + viewModel.model.pushStackElement(); + const previousCursorState = Array.from(viewModel.getCursorStates()); + const firstCursor = previousCursorState.pop(); + if (!firstCursor) { + return; + } + previousCursorState.unshift(firstCursor); + + viewModel.setCursorStates(args.source, CursorChangeReason.Explicit, previousCursorState); + viewModel.revealPrimaryCursor(args.source, true); + announceCursorChange(previousCursorState, viewModel.getCursorStates()); + } +} + registerEditorContribution(MultiCursorSelectionController.ID, MultiCursorSelectionController); registerEditorContribution(SelectionHighlighter.ID, SelectionHighlighter); @@ -1105,3 +1183,5 @@ registerEditorAction(SelectHighlightsAction); registerEditorAction(CompatChangeAll); registerEditorAction(InsertCursorAtEndOfLineSelected); registerEditorAction(InsertCursorAtTopOfLineSelected); +registerEditorAction(FocusNextCursor); +registerEditorAction(FocusPreviousCursor); diff --git a/src/vs/editor/contrib/parameterHints/browser/parameterHints.css b/src/vs/editor/contrib/parameterHints/browser/parameterHints.css index b752f49f415..98c398d2792 100644 --- a/src/vs/editor/contrib/parameterHints/browser/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/browser/parameterHints.css @@ -9,6 +9,7 @@ display: flex; flex-direction: column; line-height: 1.5em; + cursor: default; } .monaco-editor .parameter-hints-widget > .phwrapper { @@ -56,6 +57,10 @@ white-space: initial; } +.monaco-editor .parameter-hints-widget .docs .markdown-docs a:hover { + cursor: pointer; +} + .monaco-editor .parameter-hints-widget .docs .markdown-docs code { font-family: var(--monaco-monospace-font); } diff --git a/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index bbc1dd38b21..13291e31fb6 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -236,9 +236,10 @@ class RenameController implements IEditorContribution { this._bulkEditService.apply(ResourceEdit.convert(renameResult), { editor: this.editor, showPreview: inputFieldResult.wantsPreview, - label: nls.localize('label', "Renaming '{0}'", loc?.text), + label: nls.localize('label', "Renaming '{0}' to '{1}'", loc?.text, inputFieldResult.newName), code: 'undoredo.rename', - quotableLabel: nls.localize('quotableLabel', "Renaming {0}", loc?.text), + quotableLabel: nls.localize('quotableLabel', "Renaming {0} to {1}", loc?.text, inputFieldResult.newName), + respectAutoSaveConfig: true }).then(result => { if (result.ariaSummary) { alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc!.text, inputFieldResult.newName, result.ariaSummary)); diff --git a/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts index 1efa3009591..3de4e65923c 100644 --- a/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts @@ -9,35 +9,16 @@ import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { SelectionRangeProvider } from 'vs/editor/common/languages'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IModelService } from 'vs/editor/common/services/model'; import { BracketSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/browser/bracketSelections'; import { provideSelectionRanges } from 'vs/editor/contrib/smartSelect/browser/smartSelect'; import { WordSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/browser/wordSelections'; import { createModelServices } from 'vs/editor/test/common/testTextModel'; -import { MockMode, StaticLanguageSelector } from 'vs/editor/test/common/mocks/mockMode'; +import { StaticLanguageSelector } from 'vs/editor/test/common/mocks/mockMode'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; - -class MockJSMode extends MockMode { - - private static readonly _id = 'mockJSMode'; - - constructor() { - super(MockJSMode._id); - - this._register(LanguageConfigurationRegistry.register(this.languageId, { - brackets: [ - ['(', ')'], - ['{', '}'], - ['[', ']'] - ], - - onEnterRules: javascriptOnEnterRules, - wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\=\+\[\{\]\}\\\;\:\'\"\,\.\<\>\/\?\s]+)/g - })); - } -} +import { ILanguageService } from 'vs/editor/common/languages/language'; suite('SmartSelect', () => { @@ -51,26 +32,36 @@ suite('SmartSelect', () => { BracketSelectionRangeProvider._maxDuration = OriginalBracketSelectionRangeProviderMaxDuration; }); + const languageId = 'mockJSMode'; let disposables: DisposableStore; let modelService: IModelService; - let mode: MockJSMode; let providers = new LanguageFeatureRegistry<SelectionRangeProvider>(); setup(() => { disposables = new DisposableStore(); const instantiationService = createModelServices(disposables); modelService = instantiationService.get(IModelService); - mode = disposables.add(new MockJSMode()); + const languagConfigurationService = instantiationService.get(ILanguageConfigurationService); + const languageService = instantiationService.get(ILanguageService); + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languagConfigurationService.register(languageId, { + brackets: [ + ['(', ')'], + ['{', '}'], + ['[', ']'] + ], + onEnterRules: javascriptOnEnterRules, + wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\=\+\[\{\]\}\\\;\:\'\"\,\.\<\>\/\?\s]+)/g + })); }); teardown(() => { - mode.dispose(); disposables.dispose(); }); async function assertGetRangesToPosition(text: string[], lineNumber: number, column: number, ranges: Range[], selectLeadingAndTrailingWhitespace = true): Promise<void> { let uri = URI.file('test.js'); - let model = modelService.createModel(text.join('\n'), new StaticLanguageSelector(mode.languageId), uri); + let model = modelService.createModel(text.join('\n'), new StaticLanguageSelector(languageId), uri); let [actual] = await provideSelectionRanges(providers, model, [new Position(lineNumber, column)], { selectLeadingAndTrailingWhitespace }, CancellationToken.None); let actualStr = actual!.map(r => new Range(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn).toString()); let desiredStr = ranges.reverse().map(r => String(r)); @@ -217,7 +208,7 @@ suite('SmartSelect', () => { let index = value.indexOf('|'); value = value.replace('|', ''); - let model = modelService.createModel(value, new StaticLanguageSelector(mode.languageId), URI.parse('fake:lang')); + let model = modelService.createModel(value, new StaticLanguageSelector(languageId), URI.parse('fake:lang')); let pos = model.getPositionAt(index); let all = await provider.provideSelectionRanges(model, [pos], CancellationToken.None); let ranges = all![0]; diff --git a/src/vs/editor/contrib/snippet/browser/snippetController2.ts b/src/vs/editor/contrib/snippet/browser/snippetController2.ts index 6c18ce0c536..b1b4a6a1dd5 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetController2.ts @@ -11,7 +11,8 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { CompletionItem, CompletionItemKind, CompletionItemProvider } from 'vs/editor/common/languages'; +import { CompletionItem, CompletionItemKind, CompletionItemProvider, SnippetTextEdit } from 'vs/editor/common/languages'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { Choice } from 'vs/editor/contrib/snippet/browser/snippetParser'; @@ -70,7 +71,8 @@ export class SnippetController2 implements IEditorContribution { private readonly _editor: ICodeEditor, @ILogService private readonly _logService: ILogService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, ) { this._inSnippet = SnippetController2.InSnippetMode.bindTo(contextKeyService); this._hasNextTabstop = SnippetController2.HasNextTabstop.bindTo(contextKeyService); @@ -122,7 +124,7 @@ export class SnippetController2 implements IEditorContribution { if (!this._session) { this._modelVersionId = this._editor.getModel().getAlternativeVersionId(); - this._session = new SnippetSession(this._editor, template, opts); + this._session = new SnippetSession(this._editor, template, opts, this._languageConfigurationService); this._session.insert(); } else { this._session.merge(template, opts); @@ -145,7 +147,7 @@ export class SnippetController2 implements IEditorContribution { } const info = model.getWordUntilPosition(position); - const isDefaultOption = info.word === activeChoice.options[0].value; + const isAnyOfOptions = Boolean(activeChoice.options.find(o => o.value === info.word)); const suggestions: CompletionItem[] = []; for (let i = 0; i < activeChoice.options.length; i++) { const option = activeChoice.options[i]; @@ -155,7 +157,7 @@ export class SnippetController2 implements IEditorContribution { insertText: option.value, sortText: 'a'.repeat(i + 1), range: new Range(position.lineNumber, info.startColumn, position.lineNumber, info.endColumn), - filterText: isDefaultOption ? `${info.word}_${option.value}` : option.value, + filterText: isAnyOfOptions ? `${info.word}_${option.value}` : undefined, command: { id: 'jumpToNextSnippetPlaceholder', title: localize('next', 'Go to next placeholder...') } }); } @@ -329,3 +331,16 @@ registerEditorCommand(new CommandCtor({ // primary: KeyCode.Enter, // } })); + + +// --- + +export function performSnippetEdit(editor: ICodeEditor, edit: SnippetTextEdit) { + const controller = SnippetController2.get(editor); + if (!controller) { + return false; + } + editor.setSelection(edit.range); + controller.insert(edit.snippet); + return controller.isInSnippet(); +} diff --git a/src/vs/editor/contrib/snippet/browser/snippetSession.ts b/src/vs/editor/contrib/snippet/browser/snippetSession.ts index bd4edd00258..0d8fb8ada4a 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetSession.ts @@ -16,6 +16,7 @@ import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { TextChange } from 'vs/editor/common/core/textChange'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IIdentifiedSingleEditOperation, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { OvertypingCapturer } from 'vs/editor/contrib/suggest/browser/suggestOvertypingCapturer'; @@ -207,6 +208,11 @@ export class OneSnippet { return this._snippet.placeholders.length > 0; } + get isTrivialSnippet(): boolean { + return this._snippet.placeholders.length === 0 + || (this._snippet.placeholders.length === 1 && this._snippet.placeholders[0].isFinalTabstop); + } + computePossibleSelections() { const result = new Map<number, Range[]>(); for (const placeholdersWithEqualIndex of this._placeholderGroups) { @@ -413,7 +419,7 @@ export class SnippetSession { return selection; } - static createEditsAndSnippets(editor: IActiveCodeEditor, template: string, overwriteBefore: number, overwriteAfter: number, enforceFinalTabstop: boolean, adjustWhitespace: boolean, clipboardText: string | undefined, overtypingCapturer: OvertypingCapturer | undefined): { edits: IIdentifiedSingleEditOperation[]; snippets: OneSnippet[] } { + static createEditsAndSnippets(editor: IActiveCodeEditor, template: string, overwriteBefore: number, overwriteAfter: number, enforceFinalTabstop: boolean, adjustWhitespace: boolean, clipboardText: string | undefined, overtypingCapturer: OvertypingCapturer | undefined, languageConfigurationService: ILanguageConfigurationService): { edits: IIdentifiedSingleEditOperation[]; snippets: OneSnippet[] } { const edits: IIdentifiedSingleEditOperation[] = []; const snippets: OneSnippet[] = []; @@ -479,7 +485,7 @@ export class SnippetSession { modelBasedVariableResolver, new ClipboardBasedVariableResolver(readClipboardText, idx, indexedSelections.length, editor.getOption(EditorOption.multiCursorPaste) === 'spread'), new SelectionBasedVariableResolver(model, selection, idx, overtypingCapturer), - new CommentBasedVariableResolver(model, selection), + new CommentBasedVariableResolver(model, selection, languageConfigurationService), new TimeBasedVariableResolver, new WorkspaceBasedVariableResolver(workspaceService), new RandomBasedVariableResolver, @@ -503,7 +509,12 @@ export class SnippetSession { private readonly _options: ISnippetSessionInsertOptions; private _snippets: OneSnippet[] = []; - constructor(editor: IActiveCodeEditor, template: string, options: ISnippetSessionInsertOptions = _defaultOptions) { + constructor( + editor: IActiveCodeEditor, + template: string, + options: ISnippetSessionInsertOptions = _defaultOptions, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService + ) { this._editor = editor; this._template = template; this._options = options; @@ -523,7 +534,7 @@ export class SnippetSession { } // make insert edit and start with first selections - const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._options.overwriteBefore, this._options.overwriteAfter, false, this._options.adjustWhitespace, this._options.clipboardText, this._options.overtypingCapturer); + const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._options.overwriteBefore, this._options.overwriteAfter, false, this._options.adjustWhitespace, this._options.clipboardText, this._options.overtypingCapturer, this._languageConfigurationService); this._snippets = snippets; this._editor.executeEdits('snippet', edits, _undoEdits => { @@ -550,7 +561,7 @@ export class SnippetSession { return; } this._templateMerges.push([this._snippets[0]._nestingLevel, this._snippets[0]._placeholderGroupsIdx, template]); - const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, template, options.overwriteBefore, options.overwriteAfter, true, options.adjustWhitespace, options.clipboardText, options.overtypingCapturer); + const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, template, options.overwriteBefore, options.overwriteAfter, true, options.adjustWhitespace, options.clipboardText, options.overtypingCapturer, this._languageConfigurationService); this._editor.executeEdits('snippet', edits, _undoEdits => { // Sometimes, the text buffer will remove automatic whitespace when doing any edits, @@ -561,18 +572,21 @@ export class SnippetSession { snippets[idx].initialize(undoEdits[idx].textChange); } - for (const snippet of this._snippets) { - snippet.merge(snippets); + // Trivial snippets have no placeholder or are just the final placeholder. That means they + // are just text insertions and we don't need to merge the nested snippet into the existing + // snippet + const isTrivialSnippet = snippets[0].isTrivialSnippet; + if (!isTrivialSnippet) { + for (const snippet of this._snippets) { + snippet.merge(snippets); + } + console.assert(snippets.length === 0); } - console.assert(snippets.length === 0); - if (this._snippets[0].hasPlaceholder) { + if (this._snippets[0].hasPlaceholder && !isTrivialSnippet) { return this._move(undefined); } else { - return ( - undoEdits - .map(edit => Selection.fromPositions(edit.range.getEndPosition())) - ); + return undoEdits.map(edit => Selection.fromPositions(edit.range.getEndPosition())); } }); } diff --git a/src/vs/editor/contrib/snippet/browser/snippetVariables.ts b/src/vs/editor/contrib/snippet/browser/snippetVariables.ts index bd8441359b8..43825d087f9 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetVariables.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetVariables.ts @@ -10,7 +10,7 @@ import { commonPrefixLength, getLeadingWhitespace, isFalsyOrWhitespace, splitLin import { generateUuid } from 'vs/base/common/uuid'; import { Selection } from 'vs/editor/common/core/selection'; import { ITextModel } from 'vs/editor/common/model'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { Text, Variable, VariableResolver } from 'vs/editor/contrib/snippet/browser/snippetParser'; import { OvertypingCapturer } from 'vs/editor/contrib/suggest/browser/suggestOvertypingCapturer'; import * as nls from 'vs/nls'; @@ -234,14 +234,15 @@ export class ClipboardBasedVariableResolver implements VariableResolver { export class CommentBasedVariableResolver implements VariableResolver { constructor( private readonly _model: ITextModel, - private readonly _selection: Selection + private readonly _selection: Selection, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService ) { // } resolve(variable: Variable): string | undefined { const { name } = variable; const langId = this._model.getLanguageIdAtPosition(this._selection.selectionStartLineNumber, this._selection.selectionStartColumn); - const config = LanguageConfigurationRegistry.getComments(langId); + const config = this._languageConfigurationService.getLanguageConfiguration(langId).comments; if (!config) { return undefined; } diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts index 5196e6b7ef1..5529867a3f8 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetController2.old.test.ts @@ -10,6 +10,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { ITestCodeEditor, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -22,7 +23,7 @@ class TestSnippetController extends SnippetController2 { editor: ICodeEditor, @IContextKeyService private readonly _contextKeyService: IContextKeyService ) { - super(editor, new NullLogService(), new LanguageFeaturesService(), _contextKeyService); + super(editor, new NullLogService(), new LanguageFeaturesService(), _contextKeyService, new TestLanguageConfigurationService()); } isInSnippetMode(): boolean { diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts index 12525374ee1..3560a2c2be4 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts @@ -165,6 +165,21 @@ suite('SnippetController2', function () { // assertContextKeys(contextKeys, false, false, false); }); + test('insert, nested trivial snippet', function () { + const ctrl = instaService.createInstance(SnippetController2, editor); + ctrl.insert('${1:foo}bar$0'); + assertContextKeys(contextKeys, true, false, true); + assertSelections(editor, new Selection(1, 1, 1, 4), new Selection(2, 5, 2, 8)); + + ctrl.insert('FOO$0'); + assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); + assertContextKeys(contextKeys, true, false, true); + + ctrl.next(); + assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); + assertContextKeys(contextKeys, false, false, false); + }); + test('insert, nested snippet', function () { const ctrl = instaService.createInstance(SnippetController2, editor); ctrl.insert('${1:foobar}$0'); diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts index b2f953d2697..0c2f31c225b 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts @@ -8,10 +8,12 @@ import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { TextModel } from 'vs/editor/common/model/textModel'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; import { SnippetSession } from 'vs/editor/contrib/snippet/browser/snippetSession'; import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -19,6 +21,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace suite('SnippetSession', function () { + let languageConfigurationService: ILanguageConfigurationService; let editor: IActiveCodeEditor; let model: TextModel; @@ -32,9 +35,11 @@ suite('SnippetSession', function () { setup(function () { model = createTextModel('function foo() {\n console.log(a);\n}'); + languageConfigurationService = new TestLanguageConfigurationService(); const serviceCollection = new ServiceCollection( [ILabelService, new class extends mock<ILabelService>() { }], [IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() { }], + [ILanguageConfigurationService, languageConfigurationService] ); editor = createTestCodeEditor(model, { serviceCollection }) as IActiveCodeEditor; editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)]); @@ -79,7 +84,7 @@ suite('SnippetSession', function () { }); test('text edits & selection', function () { - const session = new SnippetSession(editor, 'foo${1:bar}foo$0'); + const session = new SnippetSession(editor, 'foo${1:bar}foo$0', undefined, languageConfigurationService); session.insert(); assert.strictEqual(editor.getModel()!.getValue(), 'foobarfoofunction foo() {\n foobarfooconsole.log(a);\n}'); @@ -90,7 +95,7 @@ suite('SnippetSession', function () { test('text edit with reversed selection', function () { - const session = new SnippetSession(editor, '${1:bar}$0'); + const session = new SnippetSession(editor, '${1:bar}$0', undefined, languageConfigurationService); editor.setSelections([new Selection(2, 5, 2, 5), new Selection(1, 1, 1, 1)]); session.insert(); @@ -99,7 +104,7 @@ suite('SnippetSession', function () { }); test('snippets, repeated tabstops', function () { - const session = new SnippetSession(editor, '${1:abc}foo${1:abc}$0'); + const session = new SnippetSession(editor, '${1:abc}foo${1:abc}$0', undefined, languageConfigurationService); session.insert(); assertSelections(editor, new Selection(1, 1, 1, 4), new Selection(1, 7, 1, 10), @@ -113,7 +118,7 @@ suite('SnippetSession', function () { }); test('snippets, just text', function () { - const session = new SnippetSession(editor, 'foobar'); + const session = new SnippetSession(editor, 'foobar', undefined, languageConfigurationService); session.insert(); assert.strictEqual(model.getValue(), 'foobarfunction foo() {\n foobarconsole.log(a);\n}'); assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); @@ -121,7 +126,7 @@ suite('SnippetSession', function () { test('snippets, selections and new text with newlines', () => { - const session = new SnippetSession(editor, 'foo\n\t${1:bar}\n$0'); + const session = new SnippetSession(editor, 'foo\n\t${1:bar}\n$0', undefined, languageConfigurationService); session.insert(); assert.strictEqual(editor.getModel()!.getValue(), 'foo\n bar\nfunction foo() {\n foo\n bar\n console.log(a);\n}'); @@ -135,14 +140,14 @@ suite('SnippetSession', function () { test('snippets, newline NO whitespace adjust', () => { editor.setSelection(new Selection(2, 5, 2, 5)); - const session = new SnippetSession(editor, 'abc\n foo\n bar\n$0', { overwriteBefore: 0, overwriteAfter: 0, adjustWhitespace: false, clipboardText: undefined, overtypingCapturer: undefined }); + const session = new SnippetSession(editor, 'abc\n foo\n bar\n$0', { overwriteBefore: 0, overwriteAfter: 0, adjustWhitespace: false, clipboardText: undefined, overtypingCapturer: undefined }, languageConfigurationService); session.insert(); assert.strictEqual(editor.getModel()!.getValue(), 'function foo() {\n abc\n foo\n bar\nconsole.log(a);\n}'); }); test('snippets, selections -> next/prev', () => { - const session = new SnippetSession(editor, 'f$1oo${2:bar}foo$0'); + const session = new SnippetSession(editor, 'f$1oo${2:bar}foo$0', undefined, languageConfigurationService); session.insert(); // @ $2 @@ -162,7 +167,7 @@ suite('SnippetSession', function () { }); test('snippets, selections & typing', function () { - const session = new SnippetSession(editor, 'f${1:oo}_$2_$0'); + const session = new SnippetSession(editor, 'f${1:oo}_$2_$0', undefined, languageConfigurationService); session.insert(); editor.trigger('test', 'type', { text: 'X' }); @@ -187,7 +192,7 @@ suite('SnippetSession', function () { model.setValue('foo_bar_foo'); editor.setSelections([new Selection(1, 1, 1, 4), new Selection(1, 9, 1, 12)]); - new SnippetSession(editor, 'x$0').insert(); + new SnippetSession(editor, 'x$0', undefined, languageConfigurationService).insert(); assert.strictEqual(model.getValue(), 'x_bar_x'); assertSelections(editor, new Selection(1, 2, 1, 2), new Selection(1, 8, 1, 8)); }); @@ -196,7 +201,7 @@ suite('SnippetSession', function () { model.setValue('foo_bar_foo'); editor.setSelections([new Selection(1, 1, 1, 4), new Selection(1, 9, 1, 12)]); - new SnippetSession(editor, 'LONGER$0').insert(); + new SnippetSession(editor, 'LONGER$0', undefined, languageConfigurationService).insert(); assert.strictEqual(model.getValue(), 'LONGER_bar_LONGER'); assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(1, 18, 1, 18)); }); @@ -204,7 +209,7 @@ suite('SnippetSession', function () { test('snippets, don\'t grow final tabstop', function () { model.setValue('foo_zzz_foo'); editor.setSelection(new Selection(1, 5, 1, 8)); - const session = new SnippetSession(editor, '$1bar$0'); + const session = new SnippetSession(editor, '$1bar$0', undefined, languageConfigurationService); session.insert(); assertSelections(editor, new Selection(1, 5, 1, 5)); @@ -224,7 +229,7 @@ suite('SnippetSession', function () { test('snippets, don\'t merge touching tabstops 1/2', function () { - const session = new SnippetSession(editor, '$1$2$3$0'); + const session = new SnippetSession(editor, '$1$2$3$0', undefined, languageConfigurationService); session.insert(); assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)); @@ -262,7 +267,7 @@ suite('SnippetSession', function () { }); test('snippets, don\'t merge touching tabstops 2/2', function () { - const session = new SnippetSession(editor, '$1$2$3$0'); + const session = new SnippetSession(editor, '$1$2$3$0', undefined, languageConfigurationService); session.insert(); assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)); @@ -281,7 +286,7 @@ suite('SnippetSession', function () { }); test('snippets, gracefully move over final tabstop', function () { - const session = new SnippetSession(editor, '${1}bar$0'); + const session = new SnippetSession(editor, '${1}bar$0', undefined, languageConfigurationService); session.insert(); assert.strictEqual(session.isAtLastPlaceholder, false); @@ -297,7 +302,7 @@ suite('SnippetSession', function () { }); test('snippets, overwriting nested placeholder', function () { - const session = new SnippetSession(editor, 'log(${1:"$2"});$0'); + const session = new SnippetSession(editor, 'log(${1:"$2"});$0', undefined, languageConfigurationService); session.insert(); assertSelections(editor, new Selection(1, 5, 1, 7), new Selection(2, 9, 2, 11)); @@ -314,7 +319,7 @@ suite('SnippetSession', function () { }); test('snippets, selections and snippet ranges', function () { - const session = new SnippetSession(editor, '${1:foo}farboo${2:bar}$0'); + const session = new SnippetSession(editor, '${1:foo}farboo${2:bar}$0', undefined, languageConfigurationService); session.insert(); assert.strictEqual(model.getValue(), 'foofarboobarfunction foo() {\n foofarboobarconsole.log(a);\n}'); assertSelections(editor, new Selection(1, 1, 1, 4), new Selection(2, 5, 2, 8)); @@ -350,12 +355,12 @@ suite('SnippetSession', function () { model.setValue(''); editor.setSelection(new Selection(1, 1, 1, 1)); - const first = new SnippetSession(editor, 'foo${2:bar}foo$0'); + const first = new SnippetSession(editor, 'foo${2:bar}foo$0', undefined, languageConfigurationService); first.insert(); assert.strictEqual(model.getValue(), 'foobarfoo'); assertSelections(editor, new Selection(1, 4, 1, 7)); - const second = new SnippetSession(editor, 'ba${1:zzzz}$0'); + const second = new SnippetSession(editor, 'ba${1:zzzz}$0', undefined, languageConfigurationService); second.insert(); assert.strictEqual(model.getValue(), 'foobazzzzfoo'); assertSelections(editor, new Selection(1, 6, 1, 10)); @@ -371,7 +376,7 @@ suite('SnippetSession', function () { test('snippets, typing at final tabstop', function () { - const session = new SnippetSession(editor, 'farboo$0'); + const session = new SnippetSession(editor, 'farboo$0', undefined, languageConfigurationService); session.insert(); assert.strictEqual(session.isAtLastPlaceholder, true); assert.strictEqual(session.isSelectionWithinPlaceholders(), false); @@ -383,7 +388,7 @@ suite('SnippetSession', function () { test('snippets, typing at beginning', function () { editor.setSelection(new Selection(1, 2, 1, 2)); - const session = new SnippetSession(editor, 'farboo$0'); + const session = new SnippetSession(editor, 'farboo$0', undefined, languageConfigurationService); session.insert(); editor.setSelection(new Selection(1, 2, 1, 2)); @@ -401,7 +406,7 @@ suite('SnippetSession', function () { test('snippets, typing with nested placeholder', function () { editor.setSelection(new Selection(1, 1, 1, 1)); - const session = new SnippetSession(editor, 'This ${1:is ${2:nested}}.$0'); + const session = new SnippetSession(editor, 'This ${1:is ${2:nested}}.$0', undefined, languageConfigurationService); session.insert(); assertSelections(editor, new Selection(1, 6, 1, 15)); @@ -417,7 +422,7 @@ suite('SnippetSession', function () { }); test('snippets, snippet with variables', function () { - const session = new SnippetSession(editor, '@line=$TM_LINE_NUMBER$0'); + const session = new SnippetSession(editor, '@line=$TM_LINE_NUMBER$0', undefined, languageConfigurationService); session.insert(); assert.strictEqual(model.getValue(), '@line=1function foo() {\n @line=2console.log(a);\n}'); @@ -426,7 +431,7 @@ suite('SnippetSession', function () { test('snippets, merge', function () { editor.setSelection(new Selection(1, 1, 1, 1)); - const session = new SnippetSession(editor, 'This ${1:is ${2:nested}}.$0'); + const session = new SnippetSession(editor, 'This ${1:is ${2:nested}}.$0', undefined, languageConfigurationService); session.insert(); session.next(); assertSelections(editor, new Selection(1, 9, 1, 15)); @@ -457,7 +462,7 @@ suite('SnippetSession', function () { test('snippets, transform', function () { editor.getModel()!.setValue(''); editor.setSelection(new Selection(1, 1, 1, 1)); - const session = new SnippetSession(editor, '${1/foo/bar/}$0'); + const session = new SnippetSession(editor, '${1/foo/bar/}$0', undefined, languageConfigurationService); session.insert(); assertSelections(editor, new Selection(1, 1, 1, 1)); @@ -472,7 +477,7 @@ suite('SnippetSession', function () { test('snippets, multi placeholder same index one transform', function () { editor.getModel()!.setValue(''); editor.setSelection(new Selection(1, 1, 1, 1)); - const session = new SnippetSession(editor, '$1 baz ${1/foo/bar/}$0'); + const session = new SnippetSession(editor, '$1 baz ${1/foo/bar/}$0', undefined, languageConfigurationService); session.insert(); assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(1, 6, 1, 6)); @@ -487,7 +492,7 @@ suite('SnippetSession', function () { test('snippets, transform example', function () { editor.getModel()!.setValue(''); editor.setSelection(new Selection(1, 1, 1, 1)); - const session = new SnippetSession(editor, '${1:name} : ${2:type}${3/\\s:=(.*)/${1:+ :=}${1}/};\n$0'); + const session = new SnippetSession(editor, '${1:name} : ${2:type}${3/\\s:=(.*)/${1:+ :=}${1}/};\n$0', undefined, languageConfigurationService); session.insert(); assertSelections(editor, new Selection(1, 1, 1, 5)); @@ -529,7 +534,7 @@ suite('SnippetSession', function () { editor.getModel()!.updateOptions({ insertSpaces: false }); editor.setSelection(new Selection(2, 2, 2, 2)); - const session = new SnippetSession(editor, snippet); + const session = new SnippetSession(editor, snippet, undefined, languageConfigurationService); session.insert(); assertSelections(editor, new Selection(2, 19, 2, 19), new Selection(3, 11, 3, 11), new Selection(3, 28, 3, 28)); @@ -549,7 +554,7 @@ suite('SnippetSession', function () { test('snippets, transform example hit if', function () { editor.getModel()!.setValue(''); editor.setSelection(new Selection(1, 1, 1, 1)); - const session = new SnippetSession(editor, '${1:name} : ${2:type}${3/\\s:=(.*)/${1:+ :=}${1}/};\n$0'); + const session = new SnippetSession(editor, '${1:name} : ${2:type}${3/\\s:=(.*)/${1:+ :=}${1}/};\n$0', undefined, languageConfigurationService); session.insert(); assertSelections(editor, new Selection(1, 1, 1, 5)); @@ -572,7 +577,7 @@ suite('SnippetSession', function () { test('Snippet tab stop selection issue #96545, snippets, transform adjacent to previous placeholder', function () { editor.getModel()!.setValue(''); editor.setSelection(new Selection(1, 1, 1, 1)); - const session = new SnippetSession(editor, '${1:{}${2:fff}${1/{/}/}'); + const session = new SnippetSession(editor, '${1:{}${2:fff}${1/{/}/}', undefined, languageConfigurationService); session.insert(); assertSelections(editor, new Selection(1, 1, 1, 2), new Selection(1, 5, 1, 6)); @@ -590,7 +595,7 @@ suite('SnippetSession', function () { test('Snippet tab stop selection issue #96545', function () { editor.getModel().setValue(''); - const session = new SnippetSession(editor, '${1:{}${2:fff}${1/[\\{]/}/}$0'); + const session = new SnippetSession(editor, '${1:{}${2:fff}${1/[\\{]/}/}$0', undefined, languageConfigurationService); session.insert(); assert.strictEqual(editor.getModel().getValue(), '{fff{'); @@ -602,7 +607,7 @@ suite('SnippetSession', function () { test('Snippet placeholder index incorrect after using 2+ snippets in a row that each end with a placeholder, #30769', function () { editor.getModel()!.setValue(''); editor.setSelection(new Selection(1, 1, 1, 1)); - const session = new SnippetSession(editor, 'test ${1:replaceme}'); + const session = new SnippetSession(editor, 'test ${1:replaceme}', undefined, languageConfigurationService); session.insert(); editor.trigger('test', 'type', { text: '1' }); @@ -639,7 +644,7 @@ suite('SnippetSession', function () { editor.getModel()!.updateOptions({ insertSpaces: false }); editor.setSelection(new Selection(2, 2, 3, 7)); - new SnippetSession(editor, '<div>\n\t$TM_SELECTED_TEXT\n</div>$0').insert(); + new SnippetSession(editor, '<div>\n\t$TM_SELECTED_TEXT\n</div>$0', undefined, languageConfigurationService).insert(); let expected = [ 'start', @@ -662,7 +667,7 @@ suite('SnippetSession', function () { editor.getModel()!.updateOptions({ insertSpaces: false }); editor.setSelection(new Selection(2, 2, 3, 7)); - new SnippetSession(editor, '<div>\n\t$TM_SELECTED_TEXT\n</div>$0').insert(); + new SnippetSession(editor, '<div>\n\t$TM_SELECTED_TEXT\n</div>$0', undefined, languageConfigurationService).insert(); expected = [ 'start', @@ -687,7 +692,7 @@ suite('SnippetSession', function () { assert.ok(actual.equalsSelection(new Selection(1, 9, 1, 12))); editor.setSelections([new Selection(1, 9, 1, 12)]); - new SnippetSession(editor, 'far', { overwriteBefore: 3, overwriteAfter: 0, adjustWhitespace: true, clipboardText: undefined, overtypingCapturer: undefined }).insert(); + new SnippetSession(editor, 'far', { overwriteBefore: 3, overwriteAfter: 0, adjustWhitespace: true, clipboardText: undefined, overtypingCapturer: undefined }, languageConfigurationService).insert(); assert.strictEqual(model.getValue(), 'console.far'); }); @@ -701,7 +706,7 @@ suite('SnippetSession', function () { '\tvar ${1:a} = 12;', '\tconsole.log(${1/(.*)/\n\t\t$1\n\t/})', '}' - ].join('\n')); + ].join('\n'), undefined, languageConfigurationService); session.insert(); diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidgetRenderer.ts b/src/vs/editor/contrib/suggest/browser/suggestWidgetRenderer.ts index 9823da21955..12000fceb97 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidgetRenderer.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidgetRenderer.ts @@ -7,7 +7,6 @@ import { isSafari } from 'vs/base/browser/browser'; import { $, append, hide, show } from 'vs/base/browser/dom'; import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IListRenderer } from 'vs/base/browser/ui/list/list'; -import { flatten } from 'vs/base/common/arrays'; import { Codicon, CSSIcon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { createMatches } from 'vs/base/common/filters'; @@ -190,10 +189,10 @@ export class ItemRenderer implements IListRenderer<CompletionItem, ISuggestionTe // special logic for 'folder' completion items data.icon.className = 'icon hide'; data.iconContainer.className = 'icon hide'; - labelOptions.extraClasses = flatten([ + labelOptions.extraClasses = [ getIconClasses(this._modelService, this._languageService, URI.from({ scheme: 'fake', path: element.textLabel }), FileKind.FOLDER), getIconClasses(this._modelService, this._languageService, URI.from({ scheme: 'fake', path: completion.detail }), FileKind.FOLDER) - ]); + ].flat(); } else { // normal icon data.icon.className = 'icon hide'; diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts index 729666346b0..2e2864340dd 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts @@ -16,7 +16,7 @@ import { Handler } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { CompletionItemKind, CompletionItemProvider, CompletionList, CompletionTriggerKind, EncodedTokenizationResult, IState, MetadataConsts, TokenizationRegistry } from 'vs/editor/common/languages'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { NullState } from 'vs/editor/common/languages/nullTokenize'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; @@ -70,10 +70,11 @@ suite('SuggestModel - Context', function () { class OuterMode extends MockMode { constructor( - @ILanguageService languageService: ILanguageService + @ILanguageService languageService: ILanguageService, + @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService, ) { super(OUTER_LANGUAGE_ID); - this._register(LanguageConfigurationRegistry.register(this.languageId, {})); + this._register(languageConfigurationService.register(this.languageId, {})); this._register(TokenizationRegistry.register(this.languageId, { getInitialState: (): IState => NullState, @@ -102,9 +103,11 @@ suite('SuggestModel - Context', function () { } class InnerMode extends MockMode { - constructor() { + constructor( + @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService + ) { super(INNER_LANGUAGE_ID); - this._register(LanguageConfigurationRegistry.register(this.languageId, {})); + this._register(languageConfigurationService.register(this.languageId, {})); } } diff --git a/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts b/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts index 138ebbd580d..6de5f364f53 100644 --- a/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts @@ -12,7 +12,7 @@ import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; import { DEFAULT_WORD_REGEXP } from 'vs/editor/common/core/wordHelper'; import * as languages from 'vs/editor/common/languages'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker'; import { EditorWorkerService } from 'vs/editor/browser/services/editorWorkerService'; import { IEditorWorkerHost } from 'vs/editor/common/services/editorWorkerHost'; @@ -20,39 +20,36 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { CompletionItem } from 'vs/editor/contrib/suggest/browser/suggest'; import { WordDistance } from 'vs/editor/contrib/suggest/browser/wordDistance'; -import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; -import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; +import { createCodeEditorServices, instantiateTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { instantiateTextModel } from 'vs/editor/test/common/testTextModel'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { NullLogService } from 'vs/platform/log/common/log'; import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; +import { ILanguageService } from 'vs/editor/common/languages/language'; suite('suggest, word distance', function () { - class BracketMode extends MockMode { - - private static readonly _id = 'bracketMode'; - - constructor() { - super(BracketMode._id); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'], - ] - })); - } - } let distance: WordDistance; let disposables = new DisposableStore(); setup(async function () { + const languageId = 'bracketMode'; disposables.clear(); - let mode = new BracketMode(); - let model = createTextModel('function abc(aa, ab){\na\n}', mode.languageId, undefined, URI.parse('test:///some.path')); - let editor = createTestCodeEditor(model); + const instantiationService = createCodeEditorServices(disposables); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const languageService = instantiationService.get(ILanguageService); + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ] + })); + + const model = disposables.add(instantiateTextModel(instantiationService, 'function abc(aa, ab){\na\n}', languageId, undefined, URI.parse('test:///some.path'))); + const editor = disposables.add(instantiateTestCodeEditor(instantiationService, model)); editor.updateOptions({ suggest: { localityBonus: true } }); editor.setPosition({ lineNumber: 2, column: 2 }); @@ -85,9 +82,6 @@ suite('suggest, word distance', function () { distance = await WordDistance.create(service, editor); disposables.add(service); - disposables.add(mode); - disposables.add(model); - disposables.add(editor); }); teardown(function () { diff --git a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts index eb21f65f984..326e6567180 100644 --- a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts +++ b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts @@ -6,7 +6,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { CharCode } from 'vs/base/common/charCode'; import { Codicon } from 'vs/base/common/codicons'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { InvisibleCharacters } from 'vs/base/common/strings'; @@ -495,12 +495,12 @@ export class UnicodeHighlighterHoverParticipant implements IEditorHoverParticipa }; const adjustSettings = nls.localize('unicodeHighlight.adjustSettings', 'Adjust settings'); - const contents: Array<IMarkdownString> = [{ - value: `${reason} [${adjustSettings}](command:${ShowExcludeOptions.ID}?${encodeURIComponent(JSON.stringify(adjustSettingsArgs))})`, - isTrusted: true, - }]; - - result.push(new MarkdownHover(this, d.range, contents, index++)); + const uri = `command:${ShowExcludeOptions.ID}?${encodeURIComponent(JSON.stringify(adjustSettingsArgs))}`; + const markdown = new MarkdownString('', true) + .appendMarkdown(reason) + .appendText(' ') + .appendLink(uri, adjustSettings); + result.push(new MarkdownHover(this, d.range, [markdown], index++)); } return result; } diff --git a/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts index 886329de6ab..2eaf5d3097c 100644 --- a/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts @@ -9,15 +9,15 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorCommand } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; -import { ILanguageConfigurationService, LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { deserializePipePositions, serializePipePositions, testRepeatedActionAndExtractPositions } from 'vs/editor/contrib/wordOperations/test/browser/wordTestUtils'; import { CursorWordAccessibilityLeft, CursorWordAccessibilityLeftSelect, CursorWordAccessibilityRight, CursorWordAccessibilityRightSelect, CursorWordEndLeft, CursorWordEndLeftSelect, CursorWordEndRight, CursorWordEndRightSelect, CursorWordLeft, CursorWordLeftSelect, CursorWordRight, CursorWordRightSelect, CursorWordStartLeft, CursorWordStartLeftSelect, CursorWordStartRight, CursorWordStartRightSelect, DeleteInsideWord, DeleteWordEndLeft, DeleteWordEndRight, DeleteWordLeft, DeleteWordRight, DeleteWordStartLeft, DeleteWordStartRight } from 'vs/editor/contrib/wordOperations/browser/wordOperations'; -import { StaticServiceAccessor } from 'vs/editor/contrib/wordPartOperations/test/browser/utils'; -import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; -import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; -import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { createCodeEditorServices, instantiateTestCodeEditor, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { instantiateTextModel } from 'vs/editor/test/common/testTextModel'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILanguageService } from 'vs/editor/common/languages/language'; suite('WordOperations', () => { @@ -45,13 +45,26 @@ suite('WordOperations', () => { const _deleteWordEndRight = new DeleteWordEndRight(); const _deleteInsideWord = new DeleteInsideWord(); - const serviceAccessor = new StaticServiceAccessor().withService( - ILanguageConfigurationService, - new TestLanguageConfigurationService() - ); + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let languageConfigurationService: ILanguageConfigurationService; + let languageService: ILanguageService; + + setup(() => { + disposables = new DisposableStore(); + instantiationService = createCodeEditorServices(disposables); + languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + languageService = instantiationService.get(ILanguageService); + }); + + teardown(() => { + disposables.dispose(); + }); function runEditorCommand(editor: ICodeEditor, command: EditorCommand): void { - command.runEditorCommand(serviceAccessor, editor, null); + instantiationService.invokeFunction((accessor) => { + command.runEditorCommand(accessor, editor, null); + }); } function cursorWordLeft(editor: ICodeEditor, inSelectionMode: boolean = false): void { runEditorCommand(editor, inSelectionMode ? _cursorWordLeftSelect : _cursorWordLeft); @@ -738,27 +751,20 @@ suite('WordOperations', () => { test('deleteWordLeft - issue #91855: Matching (quote, bracket, paren) doesn\'t get deleted when hitting Ctrl+Backspace', () => { const languageId = 'myTestMode'; - class TestMode extends MockMode { - constructor() { - super(languageId); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - autoClosingPairs: [ - { open: '\"', close: '\"' } - ] - })); - } - } - const mode = new TestMode(); - const model = createTextModel('a ""', languageId, undefined); + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + autoClosingPairs: [ + { open: '\"', close: '\"' } + ] + })); - withTestCodeEditor(model, { autoClosingDelete: 'always' }, (editor, _) => { - editor.setPosition(new Position(1, 4)); - deleteWordLeft(editor); assert.strictEqual(model.getLineContent(1), 'a '); - }); + const model = disposables.add(instantiateTextModel(instantiationService, 'a ""', languageId)); + const editor = disposables.add(instantiateTestCodeEditor(instantiationService, model, { autoClosingDelete: 'always' })); - model.dispose(); - mode.dispose(); + editor.setPosition(new Position(1, 4)); + deleteWordLeft(editor); + assert.strictEqual(model.getLineContent(1), 'a '); }); test('deleteInsideWord - empty line', () => { diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index f6f4600ed03..04e0f6fd2ca 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -178,7 +178,7 @@ export function onDidChangeModelLanguage(listener: (e: { readonly model: ITextMo * Create a new web worker that has model syncing capabilities built in. * Specify an AMD module to load that will `create` an object that will be proxied. */ -export function createWebWorker<T>(opts: IWebWorkerOptions): MonacoWebWorker<T> { +export function createWebWorker<T extends object>(opts: IWebWorkerOptions): MonacoWebWorker<T> { return actualCreateWebWorker<T>(StandaloneServices.get(IModelService), StandaloneServices.get(ILanguageConfigurationService), opts); } diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 13d7c693f1e..ca8a3522845 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -11,7 +11,7 @@ import { Range } from 'vs/editor/common/core/range'; import * as model from 'vs/editor/common/model'; import * as languages from 'vs/editor/common/languages'; import { LanguageConfiguration } from 'vs/editor/common/languages/languageConfiguration'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; import { ILanguageExtensionPoint, ILanguageService } from 'vs/editor/common/languages/language'; import * as standaloneEnums from 'vs/editor/common/standalone/standaloneEnums'; @@ -70,7 +70,8 @@ export function setLanguageConfiguration(languageId: string, configuration: Lang if (!languageService.isRegisteredLanguageId(languageId)) { throw new Error(`Cannot set configuration for unknown language ${languageId}`); } - return LanguageConfigurationRegistry.register(languageId, configuration, 100); + const languageConfigurationService = StandaloneServices.get(ILanguageConfigurationService); + return languageConfigurationService.register(languageId, configuration, 100); } /** diff --git a/src/vs/editor/standalone/test/browser/monarch.test.ts b/src/vs/editor/standalone/test/browser/monarch.test.ts index 4e7c94577ee..5e67bcb4cba 100644 --- a/src/vs/editor/standalone/test/browser/monarch.test.ts +++ b/src/vs/editor/standalone/test/browser/monarch.test.ts @@ -10,7 +10,7 @@ import { MonarchTokenizer } from 'vs/editor/standalone/common/monarch/monarchLex import { compile } from 'vs/editor/standalone/common/monarch/monarchCompile'; import { Token, TokenizationRegistry } from 'vs/editor/common/languages'; import { IMonarchLanguage } from 'vs/editor/standalone/common/monarch/monarchTypes'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; +import { DisposableStore } from 'vs/base/common/lifecycle'; suite('Monarch', () => { @@ -30,17 +30,16 @@ suite('Monarch', () => { } test('Ensure @rematch and nextEmbedded can be used together in Monarch grammar', () => { - const languageService = new LanguageService(); - const innerModeRegistration = ModesRegistry.registerLanguage({ - id: 'sql' - }); - const innerModeTokenizationRegistration = TokenizationRegistry.register('sql', createMonarchTokenizer(languageService, 'sql', { + const disposables = new DisposableStore(); + const languageService = disposables.add(new LanguageService()); + disposables.add(languageService.registerLanguage({ id: 'sql' })); + disposables.add(TokenizationRegistry.register('sql', createMonarchTokenizer(languageService, 'sql', { tokenizer: { root: [ [/./, 'token'] ] } - })); + }))); const SQL_QUERY_START = '(SELECT|INSERT|UPDATE|DELETE|CREATE|REPLACE|ALTER|WITH)'; const tokenizer = createMonarchTokenizer(languageService, 'test1', { tokenizer: { @@ -103,9 +102,7 @@ suite('Monarch', () => { new Token(3, 'source.test1', 'test1') ] ]); - innerModeTokenizationRegistration.dispose(); - innerModeRegistration.dispose(); - languageService.dispose(); + disposables.dispose(); }); test('microsoft/monaco-editor#1235: Empty Line Handling', () => { diff --git a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts index 1b268940fd1..50d960dd0fb 100644 --- a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts @@ -8,7 +8,6 @@ import { Color } from 'vs/base/common/color'; import { Emitter } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Token, IState, LanguageId, MetadataConsts } from 'vs/editor/common/languages'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; import { TokenTheme } from 'vs/editor/common/languages/supports/tokenization'; import { LanguageService } from 'vs/editor/common/services/languageService'; import { ILineTokens, IToken, TokenizationSupportAdapter, TokensProvider } from 'vs/editor/standalone/browser/standaloneLanguages'; @@ -121,7 +120,7 @@ suite('TokenizationSupport2Adapter', () => { const disposables = new DisposableStore(); const languageService = disposables.add(new LanguageService()); - disposables.add(ModesRegistry.registerLanguage({ id: languageId })); + disposables.add(languageService.registerLanguage({ id: languageId })); const adapter = new TokenizationSupportAdapter( languageId, new BadTokensProvider(), diff --git a/src/vs/editor/test/browser/commands/shiftCommand.test.ts b/src/vs/editor/test/browser/commands/shiftCommand.test.ts index fe5b6186816..6f29362117e 100644 --- a/src/vs/editor/test/browser/commands/shiftCommand.test.ts +++ b/src/vs/editor/test/browser/commands/shiftCommand.test.ts @@ -7,13 +7,14 @@ import * as assert from 'assert'; import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { getEditOperation, testCommand } from 'vs/editor/test/browser/testCommand'; import { withEditorModel } from 'vs/editor/test/common/testTextModel'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; /** * Create single edit operation @@ -30,9 +31,11 @@ class DocBlockCommentMode extends MockMode { private static readonly _id = 'commentMode'; - constructor() { + constructor( + @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService + ) { super(DocBlockCommentMode._id); - this._register(LanguageConfigurationRegistry.register(this.languageId, { + this._register(languageConfigurationService.register(this.languageId, { brackets: [ ['(', ')'], ['{', '}'], @@ -44,31 +47,32 @@ class DocBlockCommentMode extends MockMode { } } -function testShiftCommand(lines: string[], languageId: string | null, useTabStops: boolean, selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, languageId, selection, (sel) => new ShiftCommand(sel, { +function testShiftCommand(lines: string[], languageId: string | null, useTabStops: boolean, selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService = new TestLanguageConfigurationService()): void { + testCommand(lines, languageId, selection, (accessor, sel) => new ShiftCommand(sel, { isUnshift: false, tabSize: 4, indentSize: 4, insertSpaces: false, useTabStops: useTabStops, autoIndent: EditorAutoIndentStrategy.Full, - }), expectedLines, expectedSelection); + }, languageConfigurationService), expectedLines, expectedSelection); } -function testUnshiftCommand(lines: string[], languageId: string | null, useTabStops: boolean, selection: Selection, expectedLines: string[], expectedSelection: Selection): void { - testCommand(lines, languageId, selection, (sel) => new ShiftCommand(sel, { +function testUnshiftCommand(lines: string[], languageId: string | null, useTabStops: boolean, selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService = new TestLanguageConfigurationService()): void { + testCommand(lines, languageId, selection, (accessor, sel) => new ShiftCommand(sel, { isUnshift: true, tabSize: 4, indentSize: 4, insertSpaces: false, useTabStops: useTabStops, autoIndent: EditorAutoIndentStrategy.Full, - }), expectedLines, expectedSelection); + }, languageConfigurationService), expectedLines, expectedSelection); } -function withDockBlockCommentMode(callback: (mode: DocBlockCommentMode) => void): void { - let mode = new DocBlockCommentMode(); - callback(mode); +function withDockBlockCommentMode(callback: (mode: DocBlockCommentMode, languageConfigurationService: TestLanguageConfigurationService) => void): void { + const languageConfigurationService = new TestLanguageConfigurationService(); + let mode = new DocBlockCommentMode(languageConfigurationService); + callback(mode, languageConfigurationService); mode.dispose(); } @@ -554,7 +558,7 @@ suite('Editor Commands - ShiftCommand', () => { }); test('issue #348: indenting around doc block comments', () => { - withDockBlockCommentMode((mode) => { + withDockBlockCommentMode((mode, languageConfigurationService) => { testShiftCommand( [ @@ -574,7 +578,8 @@ suite('Editor Commands - ShiftCommand', () => { '\t */', '\tfunction hello() {}' ], - new Selection(1, 1, 5, 21) + new Selection(1, 1, 5, 21), + languageConfigurationService ); testUnshiftCommand( @@ -595,7 +600,8 @@ suite('Editor Commands - ShiftCommand', () => { ' */', 'function hello() {}' ], - new Selection(1, 1, 5, 20) + new Selection(1, 1, 5, 20), + languageConfigurationService ); testUnshiftCommand( @@ -616,14 +622,15 @@ suite('Editor Commands - ShiftCommand', () => { ' */', 'function hello() {}' ], - new Selection(1, 1, 5, 20) + new Selection(1, 1, 5, 20), + languageConfigurationService ); }); }); test('issue #1609: Wrong indentation of block comments', () => { - withDockBlockCommentMode((mode) => { + withDockBlockCommentMode((mode, languageConfigurationService) => { testShiftCommand( [ '', @@ -646,7 +653,8 @@ suite('Editor Commands - ShiftCommand', () => { '\t */', '\tvar foo = 0;' ], - new Selection(1, 1, 7, 14) + new Selection(1, 1, 7, 14), + languageConfigurationService ); }); }); @@ -670,14 +678,14 @@ suite('Editor Commands - ShiftCommand', () => { ], null, new Selection(1, 1, 13, 1), - (sel) => new ShiftCommand(sel, { + (accessor, sel) => new ShiftCommand(sel, { isUnshift: false, tabSize: 4, indentSize: 4, insertSpaces: true, useTabStops: false, autoIndent: EditorAutoIndentStrategy.Full, - }), + }, new TestLanguageConfigurationService()), [ ' Written | Numeric', ' one | 1', @@ -716,14 +724,14 @@ suite('Editor Commands - ShiftCommand', () => { ], null, new Selection(1, 1, 13, 1), - (sel) => new ShiftCommand(sel, { + (accessor, sel) => new ShiftCommand(sel, { isUnshift: true, tabSize: 4, indentSize: 4, insertSpaces: true, useTabStops: false, autoIndent: EditorAutoIndentStrategy.Full, - }), + }, new TestLanguageConfigurationService()), [ ' Written | Numeric', ' one | 1', @@ -762,14 +770,14 @@ suite('Editor Commands - ShiftCommand', () => { ], null, new Selection(1, 1, 13, 1), - (sel) => new ShiftCommand(sel, { + (accessor, sel) => new ShiftCommand(sel, { isUnshift: true, tabSize: 4, indentSize: 4, insertSpaces: false, useTabStops: false, autoIndent: EditorAutoIndentStrategy.Full, - }), + }, new TestLanguageConfigurationService()), [ ' Written | Numeric', ' one | 1', @@ -808,14 +816,14 @@ suite('Editor Commands - ShiftCommand', () => { ], null, new Selection(1, 1, 13, 1), - (sel) => new ShiftCommand(sel, { + (accessor, sel) => new ShiftCommand(sel, { isUnshift: true, tabSize: 4, indentSize: 4, insertSpaces: true, useTabStops: false, autoIndent: EditorAutoIndentStrategy.Full, - }), + }, new TestLanguageConfigurationService()), [ ' Written | Numeric', ' one | 1', @@ -843,14 +851,14 @@ suite('Editor Commands - ShiftCommand', () => { ], null, new Selection(1, 1, 1, 13), - (sel) => new ShiftCommand(sel, { + (accessor, sel) => new ShiftCommand(sel, { isUnshift: false, tabSize: 4, indentSize: 4, insertSpaces: false, useTabStops: true, autoIndent: EditorAutoIndentStrategy.Full, - }), + }, new TestLanguageConfigurationService()), [ '\tHello world!', 'another line' @@ -961,7 +969,7 @@ suite('Editor Commands - ShiftCommand', () => { insertSpaces: insertSpaces, useTabStops: true, autoIndent: EditorAutoIndentStrategy.Full, - }); + }, new TestLanguageConfigurationService()); let actual = getEditOperation(model, op); assert.deepStrictEqual(actual, expected); }); @@ -976,7 +984,7 @@ suite('Editor Commands - ShiftCommand', () => { insertSpaces: insertSpaces, useTabStops: true, autoIndent: EditorAutoIndentStrategy.Full, - }); + }, new TestLanguageConfigurationService()); let actual = getEditOperation(model, op); assert.deepStrictEqual(actual, expected); }); diff --git a/src/vs/editor/test/browser/config/editorConfiguration.test.ts b/src/vs/editor/test/browser/config/editorConfiguration.test.ts index ece7b58a566..1a50f81d495 100644 --- a/src/vs/editor/test/browser/config/editorConfiguration.test.ts +++ b/src/vs/editor/test/browser/config/editorConfiguration.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { IEnvConfiguration } from 'vs/editor/browser/config/editorConfiguration'; +import { migrateOptions } from 'vs/editor/browser/config/migrateOptions'; import { ConfigurationChangedEvent, EditorOption, IEditorHoverOptions, IQuickSuggestionsOptions } from 'vs/editor/common/config/editorOptions'; import { EditorZoom } from 'vs/editor/common/config/editorZoom'; import { TestConfiguration } from 'vs/editor/test/browser/config/testConfiguration'; @@ -223,3 +224,133 @@ suite('Common Editor Config', () => { }); }); }); + +suite('migrateOptions', () => { + function migrate(options: any): any { + migrateOptions(options); + return options; + } + + test('wordWrap', () => { + assert.deepStrictEqual(migrate({ wordWrap: true }), { wordWrap: 'on' }); + assert.deepStrictEqual(migrate({ wordWrap: false }), { wordWrap: 'off' }); + }); + test('lineNumbers', () => { + assert.deepStrictEqual(migrate({ lineNumbers: true }), { lineNumbers: 'on' }); + assert.deepStrictEqual(migrate({ lineNumbers: false }), { lineNumbers: 'off' }); + }); + test('autoClosingBrackets', () => { + assert.deepStrictEqual(migrate({ autoClosingBrackets: false }), { autoClosingBrackets: 'never', autoClosingQuotes: 'never', autoSurround: 'never' }); + }); + test('cursorBlinking', () => { + assert.deepStrictEqual(migrate({ cursorBlinking: 'visible' }), { cursorBlinking: 'solid' }); + }); + test('renderWhitespace', () => { + assert.deepStrictEqual(migrate({ renderWhitespace: true }), { renderWhitespace: 'boundary' }); + assert.deepStrictEqual(migrate({ renderWhitespace: false }), { renderWhitespace: 'none' }); + }); + test('renderLineHighlight', () => { + assert.deepStrictEqual(migrate({ renderLineHighlight: true }), { renderLineHighlight: 'line' }); + assert.deepStrictEqual(migrate({ renderLineHighlight: false }), { renderLineHighlight: 'none' }); + }); + test('acceptSuggestionOnEnter', () => { + assert.deepStrictEqual(migrate({ acceptSuggestionOnEnter: true }), { acceptSuggestionOnEnter: 'on' }); + assert.deepStrictEqual(migrate({ acceptSuggestionOnEnter: false }), { acceptSuggestionOnEnter: 'off' }); + }); + test('tabCompletion', () => { + assert.deepStrictEqual(migrate({ tabCompletion: true }), { tabCompletion: 'onlySnippets' }); + assert.deepStrictEqual(migrate({ tabCompletion: false }), { tabCompletion: 'off' }); + }); + test('suggest.filteredTypes', () => { + assert.deepStrictEqual( + migrate({ + suggest: { + filteredTypes: { + method: false, + function: false, + constructor: false, + deprecated: false, + field: false, + variable: false, + class: false, + struct: false, + interface: false, + module: false, + property: false, + event: false, + operator: false, + unit: false, + value: false, + constant: false, + enum: false, + enumMember: false, + keyword: false, + text: false, + color: false, + file: false, + reference: false, + folder: false, + typeParameter: false, + snippet: false, + } + } + }), { + suggest: { + filteredTypes: undefined, + showMethods: false, + showFunctions: false, + showConstructors: false, + showDeprecated: false, + showFields: false, + showVariables: false, + showClasses: false, + showStructs: false, + showInterfaces: false, + showModules: false, + showProperties: false, + showEvents: false, + showOperators: false, + showUnits: false, + showValues: false, + showConstants: false, + showEnums: false, + showEnumMembers: false, + showKeywords: false, + showWords: false, + showColors: false, + showFiles: false, + showReferences: false, + showFolders: false, + showTypeParameters: false, + showSnippets: false, + } + }); + }); + test('hover', () => { + assert.deepStrictEqual(migrate({ hover: true }), { hover: { enabled: true } }); + assert.deepStrictEqual(migrate({ hover: false }), { hover: { enabled: false } }); + }); + test('parameterHints', () => { + assert.deepStrictEqual(migrate({ parameterHints: true }), { parameterHints: { enabled: true } }); + assert.deepStrictEqual(migrate({ parameterHints: false }), { parameterHints: { enabled: false } }); + }); + test('autoIndent', () => { + assert.deepStrictEqual(migrate({ autoIndent: true }), { autoIndent: 'full' }); + assert.deepStrictEqual(migrate({ autoIndent: false }), { autoIndent: 'advanced' }); + }); + test('matchBrackets', () => { + assert.deepStrictEqual(migrate({ matchBrackets: true }), { matchBrackets: 'always' }); + assert.deepStrictEqual(migrate({ matchBrackets: false }), { matchBrackets: 'never' }); + }); + test('renderIndentGuides, highlightActiveIndentGuide', () => { + assert.deepStrictEqual(migrate({ renderIndentGuides: true }), { renderIndentGuides: undefined, guides: { indentation: true } }); + assert.deepStrictEqual(migrate({ renderIndentGuides: false }), { renderIndentGuides: undefined, guides: { indentation: false } }); + assert.deepStrictEqual(migrate({ highlightActiveIndentGuide: true }), { highlightActiveIndentGuide: undefined, guides: { highlightActiveIndentation: true } }); + assert.deepStrictEqual(migrate({ highlightActiveIndentGuide: false }), { highlightActiveIndentGuide: undefined, guides: { highlightActiveIndentation: false } }); + }); + + test('migration does not overwrite new setting', () => { + assert.deepStrictEqual(migrate({ renderIndentGuides: true, guides: { indentation: false } }), { renderIndentGuides: undefined, guides: { indentation: false } }); + assert.deepStrictEqual(migrate({ highlightActiveIndentGuide: true, guides: { highlightActiveIndentation: false } }), { highlightActiveIndentGuide: undefined, guides: { highlightActiveIndentation: false } }); + }); +}); diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 684014bbf21..2fad7ea8bea 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -15,18 +15,18 @@ import { EndOfLinePreference, EndOfLineSequence, ITextModel } from 'vs/editor/co import { TextModel } from 'vs/editor/common/model/textModel'; import { EncodedTokenizationResult, IState, ITokenizationSupport, MetadataConsts, StandardTokenType, TokenizationRegistry } from 'vs/editor/common/languages'; import { IndentAction, IndentationRule } from 'vs/editor/common/languages/languageConfiguration'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { NullState } from 'vs/editor/common/languages/nullTokenize'; -import { withTestCodeEditor, TestCodeEditorInstantiationOptions, ITestCodeEditor, createCodeEditorServices } from 'vs/editor/test/browser/testCodeEditor'; +import { withTestCodeEditor, TestCodeEditorInstantiationOptions, ITestCodeEditor, createCodeEditorServices, instantiateTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { IRelaxedTextModelCreationOptions, createTextModel, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; -import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { OutgoingViewModelEventKind } from 'vs/editor/common/viewModelEventDispatcher'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; import { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { URI } from 'vs/base/common/uri'; // --------- utils @@ -135,33 +135,6 @@ suite('Editor Controller - Cursor', () => { LINE4 + '\r\n' + LINE5; - // let thisModel: TextModel; - // let thisConfiguration: TestConfiguration; - // let thisViewModel: ViewModel; - // let cursor: Cursor; - - // setup(() => { - // let text = - // LINE1 + '\r\n' + - // LINE2 + '\n' + - // LINE3 + '\n' + - // LINE4 + '\r\n' + - // LINE5; - - // thisModel = createTextModel(text); - // thisConfiguration = new TestConfiguration({}); - // thisViewModel = createViewModel(thisConfiguration, thisModel); - - // cursor = new Cursor(thisConfiguration, thisModel, thisViewModel); - // }); - - // teardown(() => { - // cursor.dispose(); - // thisViewModel.dispose(); - // thisModel.dispose(); - // thisConfiguration.dispose(); - // }); - function runTest(callback: (editor: ITestCodeEditor, viewModel: ViewModel) => void): void { withTestCodeEditor(TEXT, {}, (editor, viewModel) => { callback(editor, viewModel); @@ -468,6 +441,120 @@ suite('Editor Controller - Cursor', () => { }); }); + test('issue #144041: Cursor up/down works', () => { + let model = createTextModel( + [ + 'Word1 Word2 Word3 Word4', + 'Word5 Word6 Word7 Word8', + ].join('\n') + ); + + withTestCodeEditor(model, { wrappingIndent: 'indent', wordWrap: 'wordWrapColumn', wordWrapColumn: 20 }, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 1, 1, 1)]); + + let cursorPositions: any[] = []; + function reportCursorPosition() { + cursorPositions.push(viewModel.getCursorStates()[0].viewState.position.toString()); + } + + reportCursorPosition(); + CoreNavigationCommands.CursorDown.runEditorCommand(null, editor, null); + reportCursorPosition(); + CoreNavigationCommands.CursorDown.runEditorCommand(null, editor, null); + reportCursorPosition(); + CoreNavigationCommands.CursorDown.runEditorCommand(null, editor, null); + reportCursorPosition(); + CoreNavigationCommands.CursorDown.runEditorCommand(null, editor, null); + + reportCursorPosition(); + CoreNavigationCommands.CursorUp.runEditorCommand(null, editor, null); + reportCursorPosition(); + CoreNavigationCommands.CursorUp.runEditorCommand(null, editor, null); + reportCursorPosition(); + CoreNavigationCommands.CursorUp.runEditorCommand(null, editor, null); + reportCursorPosition(); + CoreNavigationCommands.CursorUp.runEditorCommand(null, editor, null); + reportCursorPosition(); + + assert.deepStrictEqual(cursorPositions, [ + '(1,1)', + '(2,5)', + '(3,1)', + '(4,5)', + '(4,10)', + '(3,1)', + '(2,5)', + '(1,1)', + '(1,1)', + ]); + }); + + model.dispose(); + }); + + test('issue #140195: Cursor up/down makes progress', () => { + let model = createTextModel( + [ + 'Word1 Word2 Word3 Word4', + 'Word5 Word6 Word7 Word8', + ].join('\n') + ); + + withTestCodeEditor(model, { wrappingIndent: 'indent', wordWrap: 'wordWrapColumn', wordWrapColumn: 20 }, (editor, viewModel) => { + editor.deltaDecorations([], [ + { + range: new Range(1, 22, 1, 22), + options: { + showIfCollapsed: true, + description: 'test', + after: { + content: 'some very very very very very very very very long text', + } + } + } + ]); + viewModel.setSelections('test', [new Selection(1, 1, 1, 1)]); + + let cursorPositions: any[] = []; + function reportCursorPosition() { + cursorPositions.push(viewModel.getCursorStates()[0].viewState.position.toString()); + } + + reportCursorPosition(); + CoreNavigationCommands.CursorDown.runEditorCommand(null, editor, null); + reportCursorPosition(); + CoreNavigationCommands.CursorDown.runEditorCommand(null, editor, null); + reportCursorPosition(); + CoreNavigationCommands.CursorDown.runEditorCommand(null, editor, null); + reportCursorPosition(); + CoreNavigationCommands.CursorDown.runEditorCommand(null, editor, null); + + reportCursorPosition(); + CoreNavigationCommands.CursorUp.runEditorCommand(null, editor, null); + reportCursorPosition(); + CoreNavigationCommands.CursorUp.runEditorCommand(null, editor, null); + reportCursorPosition(); + CoreNavigationCommands.CursorUp.runEditorCommand(null, editor, null); + reportCursorPosition(); + CoreNavigationCommands.CursorUp.runEditorCommand(null, editor, null); + reportCursorPosition(); + + assert.deepStrictEqual(cursorPositions, [ + '(1,1)', + '(2,5)', + '(5,19)', + '(6,1)', + '(7,5)', + '(6,1)', + '(2,8)', + '(1,1)', + '(1,1)', + ]); + }); + + model.dispose(); + }); + // --------- move to beginning of line test('move to beginning of line', () => { @@ -1264,24 +1351,60 @@ suite('Editor Controller - Cursor', () => { }); }); -class SurroundingMode extends MockMode { +suite('Editor Controller', () => { + + const surroundingLanguageId = 'surroundingLanguage'; + const indentRulesLanguageId = 'indentRulesLanguage'; + const electricCharLanguageId = 'electricCharLanguage'; + const autoClosingLanguageId = 'autoClosingLanguage'; - private static readonly _id = 'surroundingMode'; + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let languageConfigurationService: ILanguageConfigurationService; + let languageService: ILanguageService; - constructor() { - super(SurroundingMode._id); - this._register(LanguageConfigurationRegistry.register(this.languageId, { + setup(() => { + disposables = new DisposableStore(); + instantiationService = createCodeEditorServices(disposables); + languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + languageService = instantiationService.get(ILanguageService); + + disposables.add(languageService.registerLanguage({ id: surroundingLanguageId })); + disposables.add(languageConfigurationService.register(surroundingLanguageId, { autoClosingPairs: [{ open: '(', close: ')' }] })); - } -} -class OnEnterMode extends MockMode { - private static readonly _id = 'onEnterMode'; + setupIndentRulesLanguage(indentRulesLanguageId, { + decreaseIndentPattern: /^\s*((?!\S.*\/[*]).*[*]\/\s*)?[})\]]|^\s*(case\b.*|default):\s*(\/\/.*|\/[*].*[*]\/\s*)?$/, + increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"']*|\[[^\]"']*|^\s*(\{\}|\(\)|\[\]|(case\b.*|default):))\s*(\/\/.*|\/[*].*[*]\/\s*)?$/, + indentNextLinePattern: /^\s*(for|while|if|else)\b(?!.*[;{}]\s*(\/\/.*|\/[*].*[*]\/\s*)?$)/, + unIndentedLinePattern: /^(?!.*([;{}]|\S:)\s*(\/\/.*|\/[*].*[*]\/\s*)?$)(?!.*(\{[^}"']*|\([^)"']*|\[[^\]"']*|^\s*(\{\}|\(\)|\[\]|(case\b.*|default):))\s*(\/\/.*|\/[*].*[*]\/\s*)?$)(?!^\s*((?!\S.*\/[*]).*[*]\/\s*)?[})\]]|^\s*(case\b.*|default):\s*(\/\/.*|\/[*].*[*]\/\s*)?$)(?!^\s*(for|while|if|else)\b(?!.*[;{}]\s*(\/\/.*|\/[*].*[*]\/\s*)?$))/ + }); - constructor(indentAction: IndentAction) { - super(OnEnterMode._id); - this._register(LanguageConfigurationRegistry.register(this.languageId, { + disposables.add(languageService.registerLanguage({ id: electricCharLanguageId })); + disposables.add(languageConfigurationService.register(electricCharLanguageId, { + __electricCharacterSupport: { + docComment: { open: '/**', close: ' */' } + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ] + })); + + setupAutoClosingLanguage(); + }); + + teardown(() => { + disposables.dispose(); + }); + + function setupOnEnterLanguage(indentAction: IndentAction): string { + const onEnterLanguageId = 'onEnterMode'; + + disposables.add(languageService.registerLanguage({ id: onEnterLanguageId })); + disposables.add(languageConfigurationService.register(onEnterLanguageId, { onEnterRules: [{ beforeText: /.*/, action: { @@ -1289,23 +1412,244 @@ class OnEnterMode extends MockMode { } }] })); + return onEnterLanguageId; } -} -class IndentRulesMode extends MockMode { - private static readonly _id = 'indentRulesMode'; - constructor(indentationRules: IndentationRule) { - super(IndentRulesMode._id); - this._register(LanguageConfigurationRegistry.register(this.languageId, { + function setupIndentRulesLanguage(languageId: string, indentationRules: IndentationRule): string { + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { indentationRules: indentationRules })); + return languageId; + } + + function setupAutoClosingLanguage() { + disposables.add(languageService.registerLanguage({ id: autoClosingLanguageId })); + disposables.add(languageConfigurationService.register(autoClosingLanguageId, { + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '\'', close: '\'', notIn: ['string', 'comment'] }, + { open: '\"', close: '\"', notIn: ['string'] }, + { open: '`', close: '`', notIn: ['string', 'comment'] }, + { open: '/**', close: ' */', notIn: ['string'] }, + { open: 'begin', close: 'end', notIn: ['string'] } + ], + __electricCharacterSupport: { + docComment: { open: '/**', close: ' */' } + } + })); + } + + function setupAutoClosingLanguageTokenization() { + class BaseState implements IState { + constructor( + public readonly parent: State | null = null + ) { } + clone(): IState { return this; } + equals(other: IState): boolean { + if (!(other instanceof BaseState)) { + return false; + } + if (!this.parent && !other.parent) { + return true; + } + if (!this.parent || !other.parent) { + return false; + } + return this.parent.equals(other.parent); + } + } + class StringState implements IState { + constructor( + public readonly char: string, + public readonly parentState: State + ) { } + clone(): IState { return this; } + equals(other: IState): boolean { return other instanceof StringState && this.char === other.char && this.parentState.equals(other.parentState); } + } + class BlockCommentState implements IState { + constructor( + public readonly parentState: State + ) { } + clone(): IState { return this; } + equals(other: IState): boolean { return other instanceof StringState && this.parentState.equals(other.parentState); } + } + type State = BaseState | StringState | BlockCommentState; + + const encodedLanguageId = languageService.languageIdCodec.encodeLanguageId(autoClosingLanguageId); + disposables.add(TokenizationRegistry.register(autoClosingLanguageId, { + getInitialState: () => new BaseState(), + tokenize: undefined!, + tokenizeEncoded: function (line: string, hasEOL: boolean, _state: IState): EncodedTokenizationResult { + let state = <State>_state; + const tokens: { length: number; type: StandardTokenType }[] = []; + const generateToken = (length: number, type: StandardTokenType, newState?: State) => { + if (tokens.length > 0 && tokens[tokens.length - 1].type === type) { + // grow last tokens + tokens[tokens.length - 1].length += length; + } else { + tokens.push({ length, type }); + } + line = line.substring(length); + if (newState) { + state = newState; + } + }; + while (line.length > 0) { + advance(); + } + let result = new Uint32Array(tokens.length * 2); + let startIndex = 0; + for (let i = 0; i < tokens.length; i++) { + result[2 * i] = startIndex; + result[2 * i + 1] = ( + (encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET) + | (tokens[i].type << MetadataConsts.TOKEN_TYPE_OFFSET) + ); + startIndex += tokens[i].length; + } + return new EncodedTokenizationResult(result, state); + + function advance(): void { + if (state instanceof BaseState) { + const m1 = line.match(/^[^'"`{}/]+/g); + if (m1) { + return generateToken(m1[0].length, StandardTokenType.Other); + } + if (/^['"`]/.test(line)) { + return generateToken(1, StandardTokenType.String, new StringState(line.charAt(0), state)); + } + if (/^{/.test(line)) { + return generateToken(1, StandardTokenType.Other, new BaseState(state)); + } + if (/^}/.test(line)) { + return generateToken(1, StandardTokenType.Other, state.parent || new BaseState()); + } + if (/^\/\//.test(line)) { + return generateToken(line.length, StandardTokenType.Comment, state); + } + if (/^\/\*/.test(line)) { + return generateToken(2, StandardTokenType.Comment, new BlockCommentState(state)); + } + return generateToken(1, StandardTokenType.Other, state); + } else if (state instanceof StringState) { + const m1 = line.match(/^[^\\'"`\$]+/g); + if (m1) { + return generateToken(m1[0].length, StandardTokenType.String); + } + if (/^\\/.test(line)) { + return generateToken(2, StandardTokenType.String); + } + if (line.charAt(0) === state.char) { + return generateToken(1, StandardTokenType.String, state.parentState); + } + if (/^\$\{/.test(line)) { + return generateToken(2, StandardTokenType.Other, new BaseState(state)); + } + return generateToken(1, StandardTokenType.Other, state); + } else if (state instanceof BlockCommentState) { + const m1 = line.match(/^[^*]+/g); + if (m1) { + return generateToken(m1[0].length, StandardTokenType.String); + } + if (/^\*\//.test(line)) { + return generateToken(2, StandardTokenType.Comment, state.parentState); + } + return generateToken(1, StandardTokenType.Other, state); + } else { + throw new Error(`unknown state`); + } + } + } + })); + } + + function setAutoClosingLanguageEnabledSet(chars: string): void { + disposables.add(languageConfigurationService.register(autoClosingLanguageId, { + autoCloseBefore: chars, + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '\'', close: '\'', notIn: ['string', 'comment'] }, + { open: '\"', close: '\"', notIn: ['string'] }, + { open: '`', close: '`', notIn: ['string', 'comment'] }, + { open: '/**', close: ' */', notIn: ['string'] } + ], + })); + } + + function createTextModel(text: string, languageId: string | null = null, options: IRelaxedTextModelCreationOptions = TextModel.DEFAULT_CREATION_OPTIONS, uri: URI | null = null): TextModel { + return disposables.add(instantiateTextModel(instantiationService, text, languageId, options, uri)); + } + + function withTestCodeEditor(text: ITextModel | string | string[], options: TestCodeEditorInstantiationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel) => void): void { + let model: ITextModel; + if (typeof text === 'string') { + model = createTextModel(text); + } else if (Array.isArray(text)) { + model = createTextModel(text.join('\n')); + } else { + model = text; + } + const editor = disposables.add(instantiateTestCodeEditor(instantiationService, model, options)); + const viewModel = editor.getViewModel()!; + viewModel.setHasFocus(true); + callback(editor, viewModel); + } + + interface ICursorOpts { + text: string[]; + languageId?: string | null; + modelOpts?: IRelaxedTextModelCreationOptions; + editorOpts?: IEditorOptions; + } + + function usingCursor(opts: ICursorOpts, callback: (editor: ITestCodeEditor, model: TextModel, viewModel: ViewModel) => void): void { + const model = createTextModel(opts.text.join('\n'), opts.languageId, opts.modelOpts); + const editorOptions: TestCodeEditorInstantiationOptions = opts.editorOpts || {}; + withTestCodeEditor(model, editorOptions, (editor, viewModel) => { + callback(editor, model, viewModel); + }); + } + + const enum AutoClosingColumnType { + Normal = 0, + Special1 = 1, + Special2 = 2 + } + + function extractAutoClosingSpecialColumns(maxColumn: number, annotatedLine: string): AutoClosingColumnType[] { + let result: AutoClosingColumnType[] = []; + for (let j = 1; j <= maxColumn; j++) { + result[j] = AutoClosingColumnType.Normal; + } + let column = 1; + for (let j = 0; j < annotatedLine.length; j++) { + if (annotatedLine.charAt(j) === '|') { + result[column] = AutoClosingColumnType.Special1; + } else if (annotatedLine.charAt(j) === '!') { + result[column] = AutoClosingColumnType.Special2; + } else { + column++; + } + } + return result; } -} -suite('Editor Controller - Regression tests', () => { + function assertType(editor: ITestCodeEditor, model: ITextModel, viewModel: ViewModel, lineNumber: number, column: number, chr: string, expectedInsert: string, message: string): void { + let lineContent = model.getLineContent(lineNumber); + let expected = lineContent.substr(0, column - 1) + expectedInsert + lineContent.substr(column - 1); + moveTo(editor, viewModel, lineNumber, column); + viewModel.type(chr, 'keyboard'); + assert.deepStrictEqual(model.getLineContent(lineNumber), expected, message); + model.undo(); + } test('issue microsoft/monaco-editor#443: Indentation of a single row deletes selected text in some cases', () => { - let model = createTextModel( + const model = createTextModel( [ 'Hello world!', 'another line' @@ -1315,7 +1659,6 @@ suite('Editor Controller - Regression tests', () => { insertSpaces: false }, ); - withTestCodeEditor(model, {}, (editor, viewModel) => { viewModel.setSelections('test', [new Selection(1, 1, 1, 13)]); @@ -1323,12 +1666,10 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 1, 1, 14)); }); - - model.dispose(); }); test('Bug 9121: Auto indent + undo + redo is funky', () => { - let model = createTextModel( + const model = createTextModel( [ '' ].join('\n'), @@ -1385,8 +1726,6 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x', 'assert15'); }); - - model.dispose(); }); test('issue #23539: Setting model EOL isn\'t undoable', () => { @@ -1410,16 +1749,12 @@ suite('Editor Controller - Regression tests', () => { test('issue #47733: Undo mangles unicode characters', () => { const languageId = 'myMode'; - class MyMode extends MockMode { - constructor() { - super(languageId); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - surroundingPairs: [{ open: '%', close: '%' }] - })); - } - } - const mode = new MyMode(); + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + surroundingPairs: [{ open: '%', close: '%' }] + })); + const model = createTextModel('\'👁\'', languageId); withTestCodeEditor(model, {}, (editor, viewModel) => { @@ -1431,13 +1766,10 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\'👁\'', 'assert2'); }); - - model.dispose(); - mode.dispose(); }); test('issue #46208: Allow empty selections in the undo/redo stack', () => { - let model = createTextModel(''); + const model = createTextModel(''); withTestCodeEditor(model, {}, (editor, viewModel) => { viewModel.type('Hello', 'keyboard'); @@ -1490,17 +1822,15 @@ suite('Editor Controller - Regression tests', () => { assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); }); - - model.dispose(); }); test('bug #16815:Shift+Tab doesn\'t go back to tabstop', () => { - let mode = new OnEnterMode(IndentAction.IndentOutdent); - let model = createTextModel( + const languageId = setupOnEnterLanguage(IndentAction.IndentOutdent); + const model = createTextModel( [ ' function baz() {' ].join('\n'), - mode.languageId + languageId ); withTestCodeEditor(model, {}, (editor, viewModel) => { @@ -1511,13 +1841,10 @@ suite('Editor Controller - Regression tests', () => { assert.strictEqual(model.getLineContent(1), ' function baz() {'); assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); - - model.dispose(); - mode.dispose(); }); test('Bug #18293:[regression][editor] Can\'t outdent whitespace line', () => { - let model = createTextModel( + const model = createTextModel( [ ' ' ].join('\n') @@ -1531,12 +1858,10 @@ suite('Editor Controller - Regression tests', () => { assert.strictEqual(model.getLineContent(1), ' '); assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); - - model.dispose(); }); test('issue #95591: Unindenting moves cursor to beginning of line', () => { - let model = createTextModel( + const model = createTextModel( [ ' ' ].join('\n') @@ -1550,12 +1875,10 @@ suite('Editor Controller - Regression tests', () => { assert.strictEqual(model.getLineContent(1), ' '); assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); - - model.dispose(); }); test('Bug #16657: [editor] Tab on empty line of zero indentation moves cursor to position (1,1)', () => { - let model = createTextModel( + const model = createTextModel( [ 'function baz() {', '\tfunction hello() { // something here', @@ -1579,8 +1902,6 @@ suite('Editor Controller - Regression tests', () => { assert.strictEqual(model.getLineContent(7), '\t'); assertCursor(viewModel, new Selection(7, 2, 7, 2)); }); - - model.dispose(); }); test('bug #16740: [editor] Cut line doesn\'t quite cut the last line', () => { @@ -1642,12 +1963,11 @@ suite('Editor Controller - Regression tests', () => { }); test('Bug #11476: Double bracket surrounding + undo is broken', () => { - let mode = new SurroundingMode(); usingCursor({ text: [ 'hello' ], - languageId: mode.languageId + languageId: surroundingLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 3, false); moveTo(editor, viewModel, 1, 5, true); @@ -1659,12 +1979,10 @@ suite('Editor Controller - Regression tests', () => { viewModel.type('(', 'keyboard'); assertCursor(viewModel, new Selection(1, 5, 1, 7)); }); - mode.dispose(); }); test('issue #1140: Backspace stops prematurely', () => { - let mode = new SurroundingMode(); - let model = createTextModel( + const model = createTextModel( [ 'function baz() {', ' return 1;', @@ -1682,9 +2000,6 @@ suite('Editor Controller - Regression tests', () => { assert.strictEqual(model.getLineCount(), 1); assert.strictEqual(model.getLineContent(1), 'function baz(;'); }); - - model.dispose(); - mode.dispose(); }); test('issue #10212: Pasting entire line does not replace selection', () => { @@ -1876,7 +2191,7 @@ suite('Editor Controller - Regression tests', () => { }); test('issue #3071: Investigate why undo stack gets corrupted', () => { - let model = createTextModel( + const model = createTextModel( [ 'some lines', 'and more lines', @@ -1922,8 +2237,6 @@ suite('Editor Controller - Regression tests', () => { 'just some text', ].join('\n'), '004'); }); - - model.dispose(); }); test('issue #12950: Cannot Double Click To Insert Emoji Using OSX Emoji Panel', () => { @@ -1948,7 +2261,7 @@ suite('Editor Controller - Regression tests', () => { }); test('issue #3463: pressing tab adds spaces, but not as many as for a tab', () => { - let model = createTextModel( + const model = createTextModel( [ 'function a() {', '\tvar a = {', @@ -1963,12 +2276,10 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.strictEqual(model.getLineContent(3), '\t \tx: 3'); }); - - model.dispose(); }); test('issue #4312: trying to type a tab character over a sequence of spaces results in unexpected behaviour', () => { - let model = createTextModel( + const model = createTextModel( [ 'var foo = 123; // this is a comment', 'var bar = 4; // another comment' @@ -1985,8 +2296,6 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.strictEqual(model.getLineContent(1), 'var foo = 123;\t// this is a comment'); }); - - model.dispose(); }); test('issue #832: word right', () => { @@ -1999,7 +2308,7 @@ suite('Editor Controller - Regression tests', () => { moveTo(editor, viewModel, 1, 1, false); function assertWordRight(col: number, expectedCol: number) { - let args = { + const args = { position: { lineNumber: 1, column: col @@ -2069,7 +2378,7 @@ suite('Editor Controller - Regression tests', () => { }); test('issue #33788: Wrong cursor position when double click to select a word', () => { - let model = createTextModel( + const model = createTextModel( [ 'Just some text' ].join('\n') @@ -2082,12 +2391,10 @@ suite('Editor Controller - Regression tests', () => { CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(viewModel, { position: new Position(1, 8) }); assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); }); - - model.dispose(); }); test('issue #12887: Double-click highlighting separating white space', () => { - let model = createTextModel( + const model = createTextModel( [ 'abc def' ].join('\n') @@ -2097,8 +2404,6 @@ suite('Editor Controller - Regression tests', () => { CoreNavigationCommands.WordSelect.runCoreEditorCommand(viewModel, { position: new Position(1, 5) }); assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 5, 1, 8)); }); - - model.dispose(); }); test('issue #9675: Undo/Redo adds a stop in between CHN Characters', () => { @@ -2132,7 +2437,7 @@ suite('Editor Controller - Regression tests', () => { this.timeout(10000); const LINE_CNT = 2000; - let text: string[] = []; + const text: string[] = []; for (let i = 0; i < LINE_CNT; i++) { text[i] = 'asd'; } @@ -2140,7 +2445,7 @@ suite('Editor Controller - Regression tests', () => { text: text }, (editor, model, viewModel) => { - let selections: Selection[] = []; + const selections: Selection[] = []; for (let i = 0; i < LINE_CNT; i++) { selections[i] = new Selection(i + 1, 1, i + 1, 1); } @@ -2410,7 +2715,7 @@ suite('Editor Controller - Regression tests', () => { }); test('issue #44805: Should not be able to undo in readonly editor', () => { - let model = createTextModel( + const model = createTextModel( [ '' ].join('\n') @@ -2426,8 +2731,6 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'Hello world!'); }); - - model.dispose(); }); test('issue #46314: ViewModel is out of sync with Model!', () => { @@ -2442,7 +2745,7 @@ suite('Editor Controller - Regression tests', () => { const LANGUAGE_ID = 'modelModeTest1'; const languageRegistration = TokenizationRegistry.register(LANGUAGE_ID, tokenizationSupport); - let model = createTextModel('Just text', LANGUAGE_ID); + const model = createTextModel('Just text', LANGUAGE_ID); withTestCodeEditor(model, {}, (editor1, cursor1) => { withTestCodeEditor(model, {}, (editor2, cursor2) => { @@ -2460,7 +2763,7 @@ suite('Editor Controller - Regression tests', () => { }); test('issue #37967: problem replacing consecutive characters', () => { - let model = createTextModel( + const model = createTextModel( [ 'const a = "foo";', 'const b = ""' @@ -2489,12 +2792,10 @@ suite('Editor Controller - Regression tests', () => { assert.strictEqual(model.getLineContent(1), 'const a = \'foo\';'); assert.strictEqual(model.getLineContent(2), 'const b = \'\''); }); - - model.dispose(); }); test('issue #15761: Cursor doesn\'t move in a redo operation', () => { - let model = createTextModel( + const model = createTextModel( [ 'hello' ].join('\n') @@ -2524,12 +2825,10 @@ suite('Editor Controller - Regression tests', () => { new Selection(1, 5, 1, 5), ]); }); - - model.dispose(); }); test('issue #42783: API Calls with Undo Leave Cursor in Wrong Position', () => { - let model = createTextModel( + const model = createTextModel( [ 'ab' ].join('\n') @@ -2561,12 +2860,10 @@ suite('Editor Controller - Regression tests', () => { new Selection(1, 1, 1, 1), ]); }); - - model.dispose(); }); test('issue #85712: Paste line moves cursor to start of current line rather than start of next line', () => { - let model = createTextModel( + const model = createTextModel( [ 'abc123', '' @@ -2585,12 +2882,10 @@ suite('Editor Controller - Regression tests', () => { ].join('\n')); assertCursor(viewModel, new Position(3, 1)); }); - - model.dispose(); }); test('issue #84897: Left delete behavior in some languages is changed', () => { - let model = createTextModel( + const model = createTextModel( [ 'สวัสดี' ].join('\n') @@ -2619,12 +2914,10 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.strictEqual(model.getValue(EndOfLinePreference.LF), ''); }); - - model.dispose(); }); test('issue #122914: Left delete behavior in some languages is changed (useTabStops: false)', () => { - let model = createTextModel( + const model = createTextModel( [ 'สวัสดี' ].join('\n') @@ -2653,8 +2946,6 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.strictEqual(model.getValue(EndOfLinePreference.LF), ''); }); - - model.dispose(); }); test('issue #99629: Emoji modifiers in text treated separately when using backspace', () => { @@ -2673,12 +2964,10 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.strictEqual(model.getValue(EndOfLinePreference.LF), ''); }); - - model.dispose(); }); test('issue #99629: Emoji modifiers in text treated separately when using backspace (ZWJ sequence)', () => { - let model = createTextModel( + const model = createTextModel( [ '👨👩🏽👧👦' ].join('\n') @@ -2702,8 +2991,6 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.strictEqual(model.getValue(EndOfLinePreference.LF), ''); }); - - model.dispose(); }); test('issue #105730: move left behaves differently for multiple cursors', () => { @@ -2740,8 +3027,6 @@ suite('Editor Controller - Regression tests', () => { new Selection(1, 32, 1, 33) ]); }); - - model.dispose(); }); test('issue #105730: move right should always skip wrap point', () => { @@ -2773,8 +3058,6 @@ suite('Editor Controller - Regression tests', () => { ]); } ); - - model.dispose(); }); test('issue #123178: sticky tab in consecutive wrapped lines', () => { @@ -2802,12 +3085,7 @@ suite('Editor Controller - Regression tests', () => { ]); } ); - - model.dispose(); }); -}); - -suite('Editor Controller - Cursor Configuration', () => { test('Cursor honors insertSpaces configuration on new line', () => { usingCursor({ @@ -2827,7 +3105,7 @@ suite('Editor Controller - Cursor Configuration', () => { }); test('Cursor honors insertSpaces configuration on tab', () => { - let model = createTextModel( + const model = createTextModel( [ ' \tMy First Line\t ', 'My Second Line123', @@ -2897,17 +3175,15 @@ suite('Editor Controller - Cursor Configuration', () => { CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.strictEqual(model.getLineContent(2), 'My Second Lin e123'); }); - - model.dispose(); }); test('Enter auto-indents with insertSpaces setting 1', () => { - let mode = new OnEnterMode(IndentAction.Indent); + const languageId = setupOnEnterLanguage(IndentAction.Indent); usingCursor({ text: [ '\thello' ], - languageId: mode.languageId + languageId: languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 7, false); assertCursor(viewModel, new Selection(1, 7, 1, 7)); @@ -2915,16 +3191,15 @@ suite('Editor Controller - Cursor Configuration', () => { viewModel.type('\n', 'keyboard'); assert.strictEqual(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); }); - mode.dispose(); }); test('Enter auto-indents with insertSpaces setting 2', () => { - let mode = new OnEnterMode(IndentAction.None); + const languageId = setupOnEnterLanguage(IndentAction.None); usingCursor({ text: [ '\thello' ], - languageId: mode.languageId + languageId: languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 7, false); assertCursor(viewModel, new Selection(1, 7, 1, 7)); @@ -2932,16 +3207,15 @@ suite('Editor Controller - Cursor Configuration', () => { viewModel.type('\n', 'keyboard'); assert.strictEqual(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); }); - mode.dispose(); }); test('Enter auto-indents with insertSpaces setting 3', () => { - let mode = new OnEnterMode(IndentAction.IndentOutdent); + const languageId = setupOnEnterLanguage(IndentAction.IndentOutdent); usingCursor({ text: [ '\thell()' ], - languageId: mode.languageId + languageId: languageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 7, false); assertCursor(viewModel, new Selection(1, 7, 1, 7)); @@ -2949,7 +3223,6 @@ suite('Editor Controller - Cursor Configuration', () => { viewModel.type('\n', 'keyboard'); assert.strictEqual(model.getValue(EndOfLinePreference.CRLF), '\thell(\r\n \r\n )'); }); - mode.dispose(); }); test('removeAutoWhitespace off', () => { @@ -2995,25 +3268,23 @@ suite('Editor Controller - Cursor Configuration', () => { }); test('issue #115033: indent and appendText', () => { - const mode = new class extends MockMode { - constructor() { - super('onEnterMode'); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - onEnterRules: [{ - beforeText: /.*/, - action: { - indentAction: IndentAction.Indent, - appendText: 'x' - } - }] - })); - } - }(); + const languageId = 'onEnterMode'; + + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + onEnterRules: [{ + beforeText: /.*/, + action: { + indentAction: IndentAction.Indent, + appendText: 'x' + } + }] + })); usingCursor({ text: [ 'text' ], - languageId: mode.languageId, + languageId: languageId, }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 5); @@ -3022,16 +3293,15 @@ suite('Editor Controller - Cursor Configuration', () => { assert.strictEqual(model.getLineContent(2), ' x'); assertCursor(viewModel, new Position(2, 6)); }); - mode.dispose(); }); test('issue #6862: Editor removes auto inserted indentation when formatting on type', () => { - let mode = new OnEnterMode(IndentAction.IndentOutdent); + const languageId = setupOnEnterLanguage(IndentAction.IndentOutdent); usingCursor({ text: [ 'function foo (params: string) {}' ], - languageId: mode.languageId, + languageId: languageId, }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 32); @@ -3060,13 +3330,12 @@ suite('Editor Controller - Cursor Configuration', () => { assert.strictEqual(model.getLineContent(2), ' '); assert.strictEqual(model.getLineContent(3), '}'); }); - mode.dispose(); }); test('removeAutoWhitespace on: removes only whitespace the cursor added 2', () => { const languageId = 'testLang'; - const registration = ModesRegistry.registerLanguage({ id: languageId }); - let model = createTextModel( + const registration = languageService.registerLanguage({ id: languageId }); + const model = createTextModel( [ ' if (a) {', ' ', @@ -3104,12 +3373,11 @@ suite('Editor Controller - Cursor Configuration', () => { assert.strictEqual(model.getLineContent(5), ' }something'); }); - model.dispose(); registration.dispose(); }); test('removeAutoWhitespace on: test 1', () => { - let model = createTextModel( + const model = createTextModel( [ ' some line abc ' ].join('\n') @@ -3161,12 +3429,10 @@ suite('Editor Controller - Cursor Configuration', () => { assert.strictEqual(model.getLineContent(4), ''); assert.strictEqual(model.getLineContent(5), ''); }); - - model.dispose(); }); test('issue #15118: remove auto whitespace when pasting entire line', () => { - let model = createTextModel( + const model = createTextModel( [ ' function f() {', ' // I\'m gonna copy this line', @@ -3200,12 +3466,10 @@ suite('Editor Controller - Cursor Configuration', () => { ].join('\n')); assertCursor(viewModel, new Position(5, 1)); }); - - model.dispose(); }); test('issue #40695: maintain cursor position when copying lines using ctrl+c, ctrl+v', () => { - let model = createTextModel( + const model = createTextModel( [ ' function f() {', ' // I\'m gonna copy this line', @@ -3230,12 +3494,10 @@ suite('Editor Controller - Cursor Configuration', () => { ].join('\n')); assertCursor(viewModel, new Position(5, 10)); }); - - model.dispose(); }); test('UseTabStops is off', () => { - let model = createTextModel( + const model = createTextModel( [ ' x', ' a ', @@ -3249,12 +3511,10 @@ suite('Editor Controller - Cursor Configuration', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.strictEqual(model.getLineContent(2), ' a '); }); - - model.dispose(); }); test('Backspace removes whitespaces with tab size', () => { - let model = createTextModel( + const model = createTextModel( [ ' \t \t x', ' a ', @@ -3315,12 +3575,10 @@ suite('Editor Controller - Cursor Configuration', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.strictEqual(model.getLineContent(2), ' a '); }); - - model.dispose(); }); test('PR #5423: Auto indent + undo + redo is funky', () => { - let model = createTextModel( + const model = createTextModel( [ '' ].join('\n'), @@ -3382,12 +3640,10 @@ suite('Editor Controller - Cursor Configuration', () => { CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x', 'assert16'); }); - - model.dispose(); }); test('issue #90973: Undo brings back model alternative version', () => { - let model = createTextModel( + const model = createTextModel( [ '' ].join('\n'), @@ -3408,19 +3664,6 @@ suite('Editor Controller - Cursor Configuration', () => { assert.notStrictEqual(beforeVersion, afterVersion); assert.strictEqual(beforeAltVersion, afterAltVersion); }); - - model.dispose(); - }); - - -}); - -suite('Editor Controller - Indentation Rules', () => { - let mode = new IndentRulesMode({ - decreaseIndentPattern: /^\s*((?!\S.*\/[*]).*[*]\/\s*)?[})\]]|^\s*(case\b.*|default):\s*(\/\/.*|\/[*].*[*]\/\s*)?$/, - increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"']*|\[[^\]"']*|^\s*(\{\}|\(\)|\[\]|(case\b.*|default):))\s*(\/\/.*|\/[*].*[*]\/\s*)?$/, - indentNextLinePattern: /^\s*(for|while|if|else)\b(?!.*[;{}]\s*(\/\/.*|\/[*].*[*]\/\s*)?$)/, - unIndentedLinePattern: /^(?!.*([;{}]|\S:)\s*(\/\/.*|\/[*].*[*]\/\s*)?$)(?!.*(\{[^}"']*|\([^)"']*|\[[^\]"']*|^\s*(\{\}|\(\)|\[\]|(case\b.*|default):))\s*(\/\/.*|\/[*].*[*]\/\s*)?$)(?!^\s*((?!\S.*\/[*]).*[*]\/\s*)?[})\]]|^\s*(case\b.*|default):\s*(\/\/.*|\/[*].*[*]\/\s*)?$)(?!^\s*(for|while|if|else)\b(?!.*[;{}]\s*(\/\/.*|\/[*].*[*]\/\s*)?$))/ }); test('Enter honors increaseIndentPattern', () => { @@ -3429,7 +3672,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true) {', '\tif (true) {' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { @@ -3454,7 +3697,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true) {', '\t' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 2, false); @@ -3472,7 +3715,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true) {', '\t\t\treturn true' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { @@ -3492,7 +3735,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true)', '\t\t\t\treturn true' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { @@ -3512,12 +3755,12 @@ suite('Editor Controller - Indentation Rules', () => { }); test('Enter honors indentNextLinePattern 2', () => { - let model = createTextModel( + const model = createTextModel( [ 'if (true)', '\tif (true)' ].join('\n'), - mode.languageId, + indentRulesLanguageId, { insertSpaces: false, } @@ -3535,8 +3778,6 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 1, 4, 1)); }); - - model.dispose(); }); test('Enter honors intential indent', () => { @@ -3547,7 +3788,7 @@ suite('Editor Controller - Indentation Rules', () => { 'return true;', '}}' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 13, false); @@ -3567,7 +3808,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\treturn true;', '\t}a}' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, modelOpts: { insertSpaces: false } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 4, 3, false); @@ -3586,7 +3827,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true) {', '\tif (true) {' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, modelOpts: { insertSpaces: false } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 12, false); @@ -3607,7 +3848,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true) {', '\tif (true) {' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 12, false); assertCursor(viewModel, new Selection(1, 12, 1, 12)); @@ -3631,7 +3872,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true) {', ' if (true) {' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 12, false); assertCursor(viewModel, new Selection(1, 12, 1, 12)); @@ -3655,7 +3896,7 @@ suite('Editor Controller - Indentation Rules', () => { 'if (true) {', ' if (true) {' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, modelOpts: { insertSpaces: false } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 12, false); @@ -3684,7 +3925,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\t}', '\t}' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { @@ -3705,7 +3946,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\treturn true;', '\t}a}' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, modelOpts: { insertSpaces: false } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 9, false); @@ -3725,7 +3966,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\treturn true;', '\t}a}' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, modelOpts: { insertSpaces: false } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 3, false); @@ -3745,7 +3986,7 @@ suite('Editor Controller - Indentation Rules', () => { ' return true;', ' }a}' ], - languageId: mode.languageId + languageId: indentRulesLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 11, false); assertCursor(viewModel, new Selection(3, 11, 3, 11)); @@ -3764,7 +4005,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\treturn true;', '\t}a}' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, modelOpts: { insertSpaces: false } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 2, false); @@ -3791,7 +4032,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t \treturn true;', '\t\t}a}' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, modelOpts: { insertSpaces: false } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 4, false); @@ -3818,7 +4059,7 @@ suite('Editor Controller - Indentation Rules', () => { ' return true;', '}a}' ], - languageId: mode.languageId + languageId: indentRulesLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 2, false); assertCursor(viewModel, new Selection(3, 2, 3, 2)); @@ -3847,7 +4088,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t return true;', '}a}' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, modelOpts: { tabSize: 2, indentSize: 2 @@ -3876,7 +4117,7 @@ suite('Editor Controller - Indentation Rules', () => { ' return true;', '' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, modelOpts: { tabSize: 2 } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 5, false); @@ -3900,7 +4141,7 @@ suite('Editor Controller - Indentation Rules', () => { modelOpts: { insertSpaces: false, }, - languageId: mode.languageId, + languageId: indentRulesLanguageId, }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 8, false); moveTo(editor, viewModel, 2, 12, true); @@ -3923,7 +4164,7 @@ suite('Editor Controller - Indentation Rules', () => { modelOpts: { insertSpaces: false, }, - languageId: mode.languageId, + languageId: indentRulesLanguageId, }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 12, false); moveTo(editor, viewModel, 3, 8, true); @@ -3976,7 +4217,7 @@ suite('Editor Controller - Indentation Rules', () => { }); test('bug #16543: Tab should indent to correct indentation spot immediately', () => { - let model = createTextModel( + const model = createTextModel( [ 'function baz() {', '\tfunction hello() { // something here', @@ -3985,7 +4226,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t}', '}' ].join('\n'), - mode.languageId, + indentRulesLanguageId, { insertSpaces: false, } @@ -3998,13 +4239,11 @@ suite('Editor Controller - Indentation Rules', () => { CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.strictEqual(model.getLineContent(4), '\t\t'); }); - - model.dispose(); }); test('bug #2938 (1): When pressing Tab on white-space only lines, indent straight to the right spot (similar to empty lines)', () => { - let model = createTextModel( + const model = createTextModel( [ '\tfunction baz() {', '\t\tfunction hello() { // something here', @@ -4013,7 +4252,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\t}', '\t}' ].join('\n'), - mode.languageId, + indentRulesLanguageId, { insertSpaces: false, } @@ -4026,13 +4265,11 @@ suite('Editor Controller - Indentation Rules', () => { CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.strictEqual(model.getLineContent(4), '\t\t\t'); }); - - model.dispose(); }); test('bug #2938 (2): When pressing Tab on white-space only lines, indent straight to the right spot (similar to empty lines)', () => { - let model = createTextModel( + const model = createTextModel( [ '\tfunction baz() {', '\t\tfunction hello() { // something here', @@ -4041,7 +4278,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\t}', '\t}' ].join('\n'), - mode.languageId, + indentRulesLanguageId, { insertSpaces: false, } @@ -4054,12 +4291,10 @@ suite('Editor Controller - Indentation Rules', () => { CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.strictEqual(model.getLineContent(4), '\t\t\t'); }); - - model.dispose(); }); test('bug #2938 (3): When pressing Tab on white-space only lines, indent straight to the right spot (similar to empty lines)', () => { - let model = createTextModel( + const model = createTextModel( [ '\tfunction baz() {', '\t\tfunction hello() { // something here', @@ -4068,7 +4303,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\t}', '\t}' ].join('\n'), - mode.languageId, + indentRulesLanguageId, { insertSpaces: false, } @@ -4081,12 +4316,10 @@ suite('Editor Controller - Indentation Rules', () => { CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.strictEqual(model.getLineContent(4), '\t\t\t\t'); }); - - model.dispose(); }); test('bug #2938 (4): When pressing Tab on white-space only lines, indent straight to the right spot (similar to empty lines)', () => { - let model = createTextModel( + const model = createTextModel( [ '\tfunction baz() {', '\t\tfunction hello() { // something here', @@ -4095,7 +4328,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\t}', '\t}' ].join('\n'), - mode.languageId, + indentRulesLanguageId, { insertSpaces: false, } @@ -4108,13 +4341,11 @@ suite('Editor Controller - Indentation Rules', () => { CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.strictEqual(model.getLineContent(4), '\t\t\t\t\t'); }); - - model.dispose(); }); test('bug #31015: When pressing Tab on lines and Enter rules are avail, indent straight to the right spotTab', () => { - let mode = new OnEnterMode(IndentAction.Indent); - let model = createTextModel( + const onEnterLanguageId = setupOnEnterLanguage(IndentAction.Indent); + const model = createTextModel( [ ' if (a) {', ' ', @@ -4122,7 +4353,7 @@ suite('Editor Controller - Indentation Rules', () => { '', ' }' ].join('\n'), - mode.languageId + onEnterLanguageId ); withTestCodeEditor(model, {}, (editor, viewModel) => { @@ -4135,23 +4366,21 @@ suite('Editor Controller - Indentation Rules', () => { assert.strictEqual(model.getLineContent(4), ''); assert.strictEqual(model.getLineContent(5), ' }'); }); - - model.dispose(); }); test('type honors indentation rules: ruby keywords', () => { - let rubyMode = new IndentRulesMode({ + const rubyLanguageId = setupIndentRulesLanguage('ruby', { increaseIndentPattern: /^\s*((begin|class|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|while)|(.*\sdo\b))\b[^\{;]*$/, decreaseIndentPattern: /^\s*([}\]]([,)]?\s*(#|$)|\.[a-zA-Z_]\w*\b)|(end|rescue|ensure|else|elsif|when)\b)/ }); - let model = createTextModel( + const model = createTextModel( [ 'class Greeter', ' def initialize(name)', ' @name = name', ' en' ].join('\n'), - rubyMode.languageId + rubyLanguageId ); withTestCodeEditor(model, { autoIndent: 'full' }, (editor, viewModel) => { @@ -4161,9 +4390,6 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('d', 'keyboard'); assert.strictEqual(model.getLineContent(4), ' end'); }); - - rubyMode.dispose(); - model.dispose(); }); test('Auto indent on type: increaseIndentPattern has higher priority than decreaseIndent when inheriting', () => { @@ -4175,7 +4401,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t\tconsole.log()', '\t}' ], - languageId: mode.languageId + languageId: indentRulesLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 5, 3, false); assertCursor(viewModel, new Selection(5, 3, 5, 3)); @@ -4196,7 +4422,7 @@ suite('Editor Controller - Indentation Rules', () => { ') {', '}' ], - languageId: mode.languageId + languageId: indentRulesLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 3, false); assertCursor(viewModel, new Selection(2, 3, 2, 3)); @@ -4214,7 +4440,7 @@ suite('Editor Controller - Indentation Rules', () => { '\t// {', '\t\t' ], - languageId: mode.languageId, + languageId: indentRulesLanguageId, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 3, false); @@ -4227,29 +4453,25 @@ suite('Editor Controller - Indentation Rules', () => { }); test('issue #36090: JS: editor.autoIndent seems to be broken', () => { - class JSMode extends MockMode { - private static readonly _id = 'indentRulesMode'; - constructor() { - super(JSMode._id); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - indentationRules: { - // ^(.*\*/)?\s*\}.*$ - decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]\)].*$/, - // ^.*\{[^}"']*$ - increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/ - }, - onEnterRules: javascriptOnEnterRules - })); - } - } + const languageId = 'jsMode'; - let mode = new JSMode(); - let model = createTextModel( + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + indentationRules: { + // ^(.*\*/)?\s*\}.*$ + decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]\)].*$/, + // ^.*\{[^}"']*$ + increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/ + }, + onEnterRules: javascriptOnEnterRules + })); + + const model = createTextModel( [ 'class ItemCtrl {', ' getPropertiesByItemId(id) {', @@ -4260,7 +4482,7 @@ suite('Editor Controller - Indentation Rules', () => { ' }', '}', ].join('\n'), - mode.languageId + languageId ); withTestCodeEditor(model, { autoIndent: 'advanced' }, (editor, viewModel) => { @@ -4283,29 +4505,22 @@ suite('Editor Controller - Indentation Rules', () => { ); assertCursor(viewModel, new Selection(8, 5, 8, 5)); }); - - model.dispose(); - mode.dispose(); }); test('issue #115304: OnEnter broken for TS', () => { - class JSMode extends MockMode { - private static readonly _id = 'indentRulesMode'; - constructor() { - super(JSMode._id); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - onEnterRules: javascriptOnEnterRules - })); - } - } + const languageId = 'jsMode'; + + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + onEnterRules: javascriptOnEnterRules + })); - const mode = new JSMode(); const model = createTextModel( [ '/** */', 'function f() {}', ].join('\n'), - mode.languageId + languageId ); withTestCodeEditor(model, { autoIndent: 'advanced' }, (editor, viewModel) => { @@ -4323,32 +4538,25 @@ suite('Editor Controller - Indentation Rules', () => { ); assertCursor(viewModel, new Selection(2, 4, 2, 4)); }); - - model.dispose(); - mode.dispose(); }); test('issue #38261: TAB key results in bizarre indentation in C++ mode ', () => { - class CppMode extends MockMode { - private static readonly _id = 'indentRulesMode'; - constructor() { - super(CppMode._id); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - indentationRules: { - increaseIndentPattern: new RegExp('^.*\\{[^}\"\\\']*$|^.*\\([^\\)\"\\\']*$|^\\s*(public|private|protected):\\s*$|^\\s*@(public|private|protected)\\s*$|^\\s*\\{\\}$'), - decreaseIndentPattern: new RegExp('^\\s*(\\s*/[*].*[*]/\\s*)*\\}|^\\s*(\\s*/[*].*[*]/\\s*)*\\)|^\\s*(public|private|protected):\\s*$|^\\s*@(public|private|protected)\\s*$'), - } - })); + const languageId = 'indentRulesMode'; + + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + indentationRules: { + increaseIndentPattern: new RegExp('^.*\\{[^}\"\\\']*$|^.*\\([^\\)\"\\\']*$|^\\s*(public|private|protected):\\s*$|^\\s*@(public|private|protected)\\s*$|^\\s*\\{\\}$'), + decreaseIndentPattern: new RegExp('^\\s*(\\s*/[*].*[*]/\\s*)*\\}|^\\s*(\\s*/[*].*[*]/\\s*)*\\)|^\\s*(public|private|protected):\\s*$|^\\s*@(public|private|protected)\\s*$'), } - } + })); - let mode = new CppMode(); - let model = createTextModel( + const model = createTextModel( [ 'int main() {', ' return 0;', @@ -4360,7 +4568,7 @@ suite('Editor Controller - Indentation Rules', () => { '', ')', ].join('\n'), - mode.languageId, + languageId, { tabSize: 2, indentSize: 2 @@ -4387,20 +4595,18 @@ suite('Editor Controller - Indentation Rules', () => { ); assert.deepStrictEqual(viewModel.getSelection(), new Selection(8, 3, 8, 3)); }); - - model.dispose(); - mode.dispose(); }); test('issue #57197: indent rules regex should be stateless', () => { + const languageId = setupIndentRulesLanguage('lang', { + decreaseIndentPattern: /^\s*}$/gm, + increaseIndentPattern: /^(?![^\S\n]*(?!--|––|——)(?:[-❍❑■⬜□☐▪▫–—≡→›✘xX✔✓☑+]|\[[ xX+-]?\])\s[^\n]*)[^\S\n]*(.+:)[^\S\n]*(?:(?=@[^\s*~(]+(?::\/\/[^\s*~(:]+)?(?:\([^)]*\))?)|$)/gm, + }); usingCursor({ text: [ 'Project:', ], - languageId: (new IndentRulesMode({ - decreaseIndentPattern: /^\s*}$/gm, - increaseIndentPattern: /^(?![^\S\n]*(?!--|––|——)(?:[-❍❑■⬜□☐▪▫–—≡→›✘xX✔✓☑+]|\[[ xX+-]?\])\s[^\n]*)[^\S\n]*(.+:)[^\S\n]*(?:(?=@[^\s*~(]+(?::\/\/[^\s*~(:]+)?(?:\([^)]*\))?)|$)/gm, - })).languageId, + languageId: languageId, modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: 'full' } }, (editor, model, viewModel) => { @@ -4419,27 +4625,23 @@ suite('Editor Controller - Indentation Rules', () => { }); }); - test('', () => { - class JSONMode extends MockMode { - private static readonly _id = 'indentRulesMode'; - constructor() { - super(JSONMode._id); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - indentationRules: { - increaseIndentPattern: new RegExp('^.*\\{[^}\"\\\']*$|^.*\\([^\\)\"\\\']*$|^\\s*(public|private|protected):\\s*$|^\\s*@(public|private|protected)\\s*$|^\\s*\\{\\}$'), - decreaseIndentPattern: new RegExp('^\\s*(\\s*/[*].*[*]/\\s*)*\\}|^\\s*(\\s*/[*].*[*]/\\s*)*\\)|^\\s*(public|private|protected):\\s*$|^\\s*@(public|private|protected)\\s*$'), - } - })); + test('typing in json', () => { + const languageId = 'indentRulesMode'; + + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + indentationRules: { + increaseIndentPattern: new RegExp('^.*\\{[^}\"\\\']*$|^.*\\([^\\)\"\\\']*$|^\\s*(public|private|protected):\\s*$|^\\s*@(public|private|protected)\\s*$|^\\s*\\{\\}$'), + decreaseIndentPattern: new RegExp('^\\s*(\\s*/[*].*[*]/\\s*)*\\}|^\\s*(\\s*/[*].*[*]/\\s*)*\\)|^\\s*(public|private|protected):\\s*$|^\\s*@(public|private|protected)\\s*$'), } - } + })); - let mode = new JSONMode(); - let model = createTextModel( + const model = createTextModel( [ '{', ' "scripts: {"', @@ -4450,7 +4652,7 @@ suite('Editor Controller - Indentation Rules', () => { ' "}"', '"}"' ].join('\n'), - mode.languageId, + languageId, { tabSize: 2, indentSize: 2 @@ -4483,13 +4685,10 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assert.deepStrictEqual(model.getLineContent(11), ' ]'); }); - - model.dispose(); - mode.dispose(); }); test('issue #111128: Multicursor `Enter` issue with indentation', () => { - const model = createTextModel(' let a, b, c;', mode.languageId, { detectIndentation: false, insertSpaces: false, tabSize: 4 }); + const model = createTextModel(' let a, b, c;', indentRulesLanguageId, { detectIndentation: false, insertSpaces: false, tabSize: 4 }); withTestCodeEditor(model, {}, (editor, viewModel) => { editor.setSelections([ new Selection(1, 11, 1, 11), @@ -4498,17 +4697,16 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assert.strictEqual(model.getValue(), ' let a,\n\t b,\n\t c;'); }); - model.dispose(); }); test('issue #122714: tabSize=1 prevent typing a string matching decreaseIndentPattern in an empty file', () => { - let latexMode = new IndentRulesMode({ + const latextLanguageId = setupIndentRulesLanguage('latex', { increaseIndentPattern: new RegExp('\\\\begin{(?!document)([^}]*)}(?!.*\\\\end{\\1})'), decreaseIndentPattern: new RegExp('^\\s*\\\\end{(?!document)') }); - let model = createTextModel( + const model = createTextModel( '\\end', - latexMode.languageId, + latextLanguageId, { tabSize: 1 } ); @@ -4519,98 +4717,51 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('{', 'keyboard'); assert.strictEqual(model.getLineContent(1), '\\end{}'); }); - - latexMode.dispose(); - model.dispose(); }); -}); -interface ICursorOpts { - text: string[]; - languageId?: string | null; - modelOpts?: IRelaxedTextModelCreationOptions; - editorOpts?: IEditorOptions; -} - -function usingCursor(opts: ICursorOpts, callback: (editor: ITestCodeEditor, model: TextModel, viewModel: ViewModel) => void): void { - const model = createTextModel(opts.text.join('\n'), opts.languageId, opts.modelOpts); - const editorOptions: TestCodeEditorInstantiationOptions = opts.editorOpts || {}; - withTestCodeEditor(model, editorOptions, (editor, viewModel) => { - callback(editor, model, viewModel); - }); - model.dispose(); -} - -class ElectricCharMode extends MockMode { - - private static readonly _id = 'electricCharMode'; - - constructor() { - super(ElectricCharMode._id); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - __electricCharacterSupport: { - docComment: { open: '/**', close: ' */' } - }, - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ] - })); - } -} - -suite('ElectricCharacter', () => { - test('does nothing if no electric char', () => { - let mode = new ElectricCharMode(); + test('ElectricCharacter - does nothing if no electric char', () => { usingCursor({ text: [ ' if (a) {', '' ], - languageId: mode.languageId + languageId: electricCharLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('*', 'keyboard'); assert.deepStrictEqual(model.getLineContent(2), '*'); }); - mode.dispose(); }); - test('indents in order to match bracket', () => { - let mode = new ElectricCharMode(); + test('ElectricCharacter - indents in order to match bracket', () => { usingCursor({ text: [ ' if (a) {', '' ], - languageId: mode.languageId + languageId: electricCharLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('}', 'keyboard'); assert.deepStrictEqual(model.getLineContent(2), ' }'); }); - mode.dispose(); }); - test('unindents in order to match bracket', () => { - let mode = new ElectricCharMode(); + test('ElectricCharacter - unindents in order to match bracket', () => { usingCursor({ text: [ ' if (a) {', ' ' ], - languageId: mode.languageId + languageId: electricCharLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 5); viewModel.type('}', 'keyboard'); assert.deepStrictEqual(model.getLineContent(2), ' }'); }); - mode.dispose(); }); - test('matches with correct bracket', () => { - let mode = new ElectricCharMode(); + test('ElectricCharacter - matches with correct bracket', () => { usingCursor({ text: [ ' if (a) {', @@ -4618,17 +4769,15 @@ suite('ElectricCharacter', () => { ' }', ' ' ], - languageId: mode.languageId + languageId: electricCharLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 4, 1); viewModel.type('}', 'keyboard'); assert.deepStrictEqual(model.getLineContent(4), ' } '); }); - mode.dispose(); }); - test('does nothing if bracket does not match', () => { - let mode = new ElectricCharMode(); + test('ElectricCharacter - does nothing if bracket does not match', () => { usingCursor({ text: [ ' if (a) {', @@ -4636,87 +4785,77 @@ suite('ElectricCharacter', () => { ' }', ' } ' ], - languageId: mode.languageId + languageId: electricCharLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 4, 6); viewModel.type('}', 'keyboard'); assert.deepStrictEqual(model.getLineContent(4), ' } }'); }); - mode.dispose(); }); - test('matches bracket even in line with content', () => { - let mode = new ElectricCharMode(); + test('ElectricCharacter - matches bracket even in line with content', () => { usingCursor({ text: [ ' if (a) {', '// hello' ], - languageId: mode.languageId + languageId: electricCharLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('}', 'keyboard'); assert.deepStrictEqual(model.getLineContent(2), ' }// hello'); }); - mode.dispose(); }); - test('is no-op if bracket is lined up', () => { - let mode = new ElectricCharMode(); + test('ElectricCharacter - is no-op if bracket is lined up', () => { usingCursor({ text: [ ' if (a) {', ' ' ], - languageId: mode.languageId + languageId: electricCharLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 3); viewModel.type('}', 'keyboard'); assert.deepStrictEqual(model.getLineContent(2), ' }'); }); - mode.dispose(); }); - test('is no-op if there is non-whitespace text before', () => { - let mode = new ElectricCharMode(); + test('ElectricCharacter - is no-op if there is non-whitespace text before', () => { usingCursor({ text: [ ' if (a) {', 'a' ], - languageId: mode.languageId + languageId: electricCharLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 2); viewModel.type('}', 'keyboard'); assert.deepStrictEqual(model.getLineContent(2), 'a}'); }); - mode.dispose(); }); - test('is no-op if pairs are all matched before', () => { - let mode = new ElectricCharMode(); + test('ElectricCharacter - is no-op if pairs are all matched before', () => { usingCursor({ text: [ 'foo(() => {', ' ( 1 + 2 ) ', '})' ], - languageId: mode.languageId + languageId: electricCharLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 13); viewModel.type('*', 'keyboard'); assert.deepStrictEqual(model.getLineContent(2), ' ( 1 + 2 ) *'); }); - mode.dispose(); }); - test('is no-op if matching bracket is on the same line', () => { - let mode = new ElectricCharMode(); + test('ElectricCharacter - is no-op if matching bracket is on the same line', () => { usingCursor({ text: [ '(div', ], - languageId: mode.languageId + languageId: electricCharLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, 5); let changeText: string | null = null; @@ -4727,292 +4866,81 @@ suite('ElectricCharacter', () => { assert.deepStrictEqual(model.getLineContent(1), '(div)'); assert.deepStrictEqual(changeText, ')'); }); - mode.dispose(); }); - test('is no-op if the line has other content', () => { - let mode = new ElectricCharMode(); + test('ElectricCharacter - is no-op if the line has other content', () => { usingCursor({ text: [ 'Math.max(', '\t2', '\t3' ], - languageId: mode.languageId + languageId: electricCharLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 3); viewModel.type(')', 'keyboard'); assert.deepStrictEqual(model.getLineContent(3), '\t3)'); }); - mode.dispose(); }); - test('appends text', () => { - let mode = new ElectricCharMode(); + test('ElectricCharacter - appends text', () => { usingCursor({ text: [ ' if (a) {', '/*' ], - languageId: mode.languageId + languageId: electricCharLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 3); viewModel.type('*', 'keyboard'); assert.deepStrictEqual(model.getLineContent(2), '/** */'); }); - mode.dispose(); }); - test('appends text 2', () => { - let mode = new ElectricCharMode(); + test('ElectricCharacter - appends text 2', () => { usingCursor({ text: [ ' if (a) {', ' /*' ], - languageId: mode.languageId + languageId: electricCharLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 5); viewModel.type('*', 'keyboard'); assert.deepStrictEqual(model.getLineContent(2), ' /** */'); }); - mode.dispose(); }); - test('issue #23711: Replacing selected text with )]} fails to delete old text with backwards-dragged selection', () => { - let mode = new ElectricCharMode(); + test('ElectricCharacter - issue #23711: Replacing selected text with )]} fails to delete old text with backwards-dragged selection', () => { usingCursor({ text: [ '{', 'word' ], - languageId: mode.languageId + languageId: electricCharLanguageId }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 5); moveTo(editor, viewModel, 2, 1, true); viewModel.type('}', 'keyboard'); assert.deepStrictEqual(model.getLineContent(2), '}'); }); - mode.dispose(); }); -}); - -suite('autoClosingPairs', () => { - - class AutoClosingMode extends MockMode { - - private static readonly _id = 'autoClosingMode'; - - constructor(languageService: ILanguageService | null = null) { - super(AutoClosingMode._id); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - autoClosingPairs: [ - { open: '{', close: '}' }, - { open: '[', close: ']' }, - { open: '(', close: ')' }, - { open: '\'', close: '\'', notIn: ['string', 'comment'] }, - { open: '\"', close: '\"', notIn: ['string'] }, - { open: '`', close: '`', notIn: ['string', 'comment'] }, - { open: '/**', close: ' */', notIn: ['string'] }, - { open: 'begin', close: 'end', notIn: ['string'] } - ], - __electricCharacterSupport: { - docComment: { open: '/**', close: ' */' } - } - })); - class BaseState implements IState { - constructor( - public readonly parent: State | null = null - ) { } - clone(): IState { return this; } - equals(other: IState): boolean { - if (!(other instanceof BaseState)) { - return false; - } - if (!this.parent && !other.parent) { - return true; - } - if (!this.parent || !other.parent) { - return false; - } - return this.parent.equals(other.parent); - } - } - class StringState implements IState { - constructor( - public readonly char: string, - public readonly parentState: State - ) { } - clone(): IState { return this; } - equals(other: IState): boolean { return other instanceof StringState && this.char === other.char && this.parentState.equals(other.parentState); } - } - class BlockCommentState implements IState { - constructor( - public readonly parentState: State - ) { } - clone(): IState { return this; } - equals(other: IState): boolean { return other instanceof StringState && this.parentState.equals(other.parentState); } - } - type State = BaseState | StringState | BlockCommentState; - - if (languageService) { - const encodedLanguageId = languageService.languageIdCodec.encodeLanguageId(this.languageId); - this._register(TokenizationRegistry.register(this.languageId, { - getInitialState: () => new BaseState(), - tokenize: undefined!, - tokenizeEncoded: function (line: string, hasEOL: boolean, _state: IState): EncodedTokenizationResult { - let state = <State>_state; - const tokens: { length: number; type: StandardTokenType }[] = []; - const generateToken = (length: number, type: StandardTokenType, newState?: State) => { - if (tokens.length > 0 && tokens[tokens.length - 1].type === type) { - // grow last tokens - tokens[tokens.length - 1].length += length; - } else { - tokens.push({ length, type }); - } - line = line.substring(length); - if (newState) { - state = newState; - } - }; - while (line.length > 0) { - advance(); - } - let result = new Uint32Array(tokens.length * 2); - let startIndex = 0; - for (let i = 0; i < tokens.length; i++) { - result[2 * i] = startIndex; - result[2 * i + 1] = ( - (encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET) - | (tokens[i].type << MetadataConsts.TOKEN_TYPE_OFFSET) - ); - startIndex += tokens[i].length; - } - return new EncodedTokenizationResult(result, state); - - function advance(): void { - if (state instanceof BaseState) { - const m1 = line.match(/^[^'"`{}/]+/g); - if (m1) { - return generateToken(m1[0].length, StandardTokenType.Other); - } - if (/^['"`]/.test(line)) { - return generateToken(1, StandardTokenType.String, new StringState(line.charAt(0), state)); - } - if (/^{/.test(line)) { - return generateToken(1, StandardTokenType.Other, new BaseState(state)); - } - if (/^}/.test(line)) { - return generateToken(1, StandardTokenType.Other, state.parent || new BaseState()); - } - if (/^\/\//.test(line)) { - return generateToken(line.length, StandardTokenType.Comment, state); - } - if (/^\/\*/.test(line)) { - return generateToken(2, StandardTokenType.Comment, new BlockCommentState(state)); - } - return generateToken(1, StandardTokenType.Other, state); - } else if (state instanceof StringState) { - const m1 = line.match(/^[^\\'"`\$]+/g); - if (m1) { - return generateToken(m1[0].length, StandardTokenType.String); - } - if (/^\\/.test(line)) { - return generateToken(2, StandardTokenType.String); - } - if (line.charAt(0) === state.char) { - return generateToken(1, StandardTokenType.String, state.parentState); - } - if (/^\$\{/.test(line)) { - return generateToken(2, StandardTokenType.Other, new BaseState(state)); - } - return generateToken(1, StandardTokenType.Other, state); - } else if (state instanceof BlockCommentState) { - const m1 = line.match(/^[^*]+/g); - if (m1) { - return generateToken(m1[0].length, StandardTokenType.String); - } - if (/^\*\//.test(line)) { - return generateToken(2, StandardTokenType.Comment, state.parentState); - } - return generateToken(1, StandardTokenType.Other, state); - } else { - throw new Error(`unknown state`); - } - } - } - })); - } - } - - public setAutocloseEnabledSet(chars: string) { - this._register(LanguageConfigurationRegistry.register(this.languageId, { - autoCloseBefore: chars, - autoClosingPairs: [ - { open: '{', close: '}' }, - { open: '[', close: ']' }, - { open: '(', close: ')' }, - { open: '\'', close: '\'', notIn: ['string', 'comment'] }, - { open: '\"', close: '\"', notIn: ['string'] }, - { open: '`', close: '`', notIn: ['string', 'comment'] }, - { open: '/**', close: ' */', notIn: ['string'] } - ], - })); - } - } - - const enum ColumnType { - Normal = 0, - Special1 = 1, - Special2 = 2 - } - - function extractSpecialColumns(maxColumn: number, annotatedLine: string): ColumnType[] { - let result: ColumnType[] = []; - for (let j = 1; j <= maxColumn; j++) { - result[j] = ColumnType.Normal; - } - let column = 1; - for (let j = 0; j < annotatedLine.length; j++) { - if (annotatedLine.charAt(j) === '|') { - result[column] = ColumnType.Special1; - } else if (annotatedLine.charAt(j) === '!') { - result[column] = ColumnType.Special2; - } else { - column++; - } - } - return result; - } - - function assertType(editor: ITestCodeEditor, model: ITextModel, viewModel: ViewModel, lineNumber: number, column: number, chr: string, expectedInsert: string, message: string): void { - let lineContent = model.getLineContent(lineNumber); - let expected = lineContent.substr(0, column - 1) + expectedInsert + lineContent.substr(column - 1); - moveTo(editor, viewModel, lineNumber, column); - viewModel.type(chr, 'keyboard'); - assert.deepStrictEqual(model.getLineContent(lineNumber), expected, message); - model.undo(); - } test('issue #61070: backtick (`) should auto-close after a word character', () => { - let mode = new AutoClosingMode(); usingCursor({ text: ['const markup = highlight'], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { model.forceTokenization(1); assertType(editor, model, viewModel, 1, 25, '`', '``', `auto closes \` @ (1, 25)`); }); - mode.dispose(); }); test('issue #132912: quotes should not auto-close if they are closing a string', () => { - const disposables = new DisposableStore(); - const instantiationService = createCodeEditorServices(disposables); - const languageService = instantiationService.get(ILanguageService); - const mode = disposables.add(new AutoClosingMode(languageService)); + setupAutoClosingLanguageTokenization(); + const model = createTextModel('const t2 = `something ${t1}', autoClosingLanguageId); withTestCodeEditor( - disposables.add(instantiateTextModel(instantiationService, 'const t2 = `something ${t1}', mode.languageId)), + model, {}, (editor, viewModel) => { const model = viewModel.model; @@ -5020,11 +4948,9 @@ suite('autoClosingPairs', () => { assertType(editor, model, viewModel, 1, 28, '`', '`', `does not auto close \` @ (1, 28)`); } ); - disposables.dispose(); }); - test('open parens: default', () => { - let mode = new AutoClosingMode(); + test('autoClosingPairs - open parens: default', () => { usingCursor({ text: [ 'var a = [];', @@ -5036,7 +4962,7 @@ suite('autoClosingPairs', () => { 'var g = (3+5);', 'var h = { a: \'value\' };', ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { let autoClosePositions = [ @@ -5051,11 +4977,11 @@ suite('autoClosingPairs', () => { ]; for (let i = 0, len = autoClosePositions.length; i < len; i++) { const lineNumber = i + 1; - const autoCloseColumns = extractSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); + const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { model.forceTokenization(lineNumber); - if (autoCloseColumns[column] === ColumnType.Special1) { + if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { assertType(editor, model, viewModel, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); @@ -5063,11 +4989,9 @@ suite('autoClosingPairs', () => { } } }); - mode.dispose(); }); - test('open parens: whitespace', () => { - let mode = new AutoClosingMode(); + test('autoClosingPairs - open parens: whitespace', () => { usingCursor({ text: [ 'var a = [];', @@ -5079,7 +5003,7 @@ suite('autoClosingPairs', () => { 'var g = (3+5);', 'var h = { a: \'value\' };', ], - languageId: mode.languageId, + languageId: autoClosingLanguageId, editorOpts: { autoClosingBrackets: 'beforeWhitespace' } @@ -5097,11 +5021,11 @@ suite('autoClosingPairs', () => { ]; for (let i = 0, len = autoClosePositions.length; i < len; i++) { const lineNumber = i + 1; - const autoCloseColumns = extractSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); + const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { model.forceTokenization(lineNumber); - if (autoCloseColumns[column] === ColumnType.Special1) { + if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { assertType(editor, model, viewModel, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); @@ -5109,16 +5033,14 @@ suite('autoClosingPairs', () => { } } }); - mode.dispose(); }); - test('open parens disabled/enabled open quotes enabled/disabled', () => { - let mode = new AutoClosingMode(); + test('autoClosingPairs - open parens disabled/enabled open quotes enabled/disabled', () => { usingCursor({ text: [ 'var a = [];', ], - languageId: mode.languageId, + languageId: autoClosingLanguageId, editorOpts: { autoClosingBrackets: 'beforeWhitespace', autoClosingQuotes: 'never' @@ -5130,11 +5052,11 @@ suite('autoClosingPairs', () => { ]; for (let i = 0, len = autoClosePositions.length; i < len; i++) { const lineNumber = i + 1; - const autoCloseColumns = extractSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); + const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { model.forceTokenization(lineNumber); - if (autoCloseColumns[column] === ColumnType.Special1) { + if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { assertType(editor, model, viewModel, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); @@ -5148,7 +5070,7 @@ suite('autoClosingPairs', () => { text: [ 'var b = [];', ], - languageId: mode.languageId, + languageId: autoClosingLanguageId, editorOpts: { autoClosingBrackets: 'never', autoClosingQuotes: 'beforeWhitespace' @@ -5160,11 +5082,11 @@ suite('autoClosingPairs', () => { ]; for (let i = 0, len = autoClosePositions.length; i < len; i++) { const lineNumber = i + 1; - const autoCloseColumns = extractSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); + const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { model.forceTokenization(lineNumber); - if (autoCloseColumns[column] === ColumnType.Special1) { + if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '\'', '\'\'', `auto closes @ (${lineNumber}, ${column})`); } else { assertType(editor, model, viewModel, lineNumber, column, '\'', '\'', `does not auto close @ (${lineNumber}, ${column})`); @@ -5173,12 +5095,10 @@ suite('autoClosingPairs', () => { } } }); - mode.dispose(); }); - test('configurable open parens', () => { - let mode = new AutoClosingMode(); - mode.setAutocloseEnabledSet('abc'); + test('autoClosingPairs - configurable open parens', () => { + setAutoClosingLanguageEnabledSet('abc'); usingCursor({ text: [ 'var a = [];', @@ -5190,7 +5110,7 @@ suite('autoClosingPairs', () => { 'var g = (3+5);', 'var h = { a: \'value\' };', ], - languageId: mode.languageId, + languageId: autoClosingLanguageId, editorOpts: { autoClosingBrackets: 'languageDefined' } @@ -5208,11 +5128,11 @@ suite('autoClosingPairs', () => { ]; for (let i = 0, len = autoClosePositions.length; i < len; i++) { const lineNumber = i + 1; - const autoCloseColumns = extractSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); + const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { model.forceTokenization(lineNumber); - if (autoCloseColumns[column] === ColumnType.Special1) { + if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { assertType(editor, model, viewModel, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); @@ -5220,11 +5140,9 @@ suite('autoClosingPairs', () => { } } }); - mode.dispose(); }); - test('auto-pairing can be disabled', () => { - let mode = new AutoClosingMode(); + test('autoClosingPairs - auto-pairing can be disabled', () => { usingCursor({ text: [ 'var a = [];', @@ -5236,7 +5154,7 @@ suite('autoClosingPairs', () => { 'var g = (3+5);', 'var h = { a: \'value\' };', ], - languageId: mode.languageId, + languageId: autoClosingLanguageId, editorOpts: { autoClosingBrackets: 'never', autoClosingQuotes: 'never' @@ -5255,11 +5173,11 @@ suite('autoClosingPairs', () => { ]; for (let i = 0, len = autoClosePositions.length; i < len; i++) { const lineNumber = i + 1; - const autoCloseColumns = extractSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); + const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { model.forceTokenization(lineNumber); - if (autoCloseColumns[column] === ColumnType.Special1) { + 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})`); } else { @@ -5269,16 +5187,14 @@ suite('autoClosingPairs', () => { } } }); - mode.dispose(); }); - test('auto wrapping is configurable', () => { - let mode = new AutoClosingMode(); + test('autoClosingPairs - auto wrapping is configurable', () => { usingCursor({ text: [ 'var a = asd' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [ @@ -5301,7 +5217,7 @@ suite('autoClosingPairs', () => { text: [ 'var a = asd' ], - languageId: mode.languageId, + languageId: autoClosingLanguageId, editorOpts: { autoSurround: 'never' } @@ -5321,7 +5237,7 @@ suite('autoClosingPairs', () => { text: [ 'var a = asd' ], - languageId: mode.languageId, + languageId: autoClosingLanguageId, editorOpts: { autoSurround: 'quotes' } @@ -5344,7 +5260,7 @@ suite('autoClosingPairs', () => { text: [ 'var a = asd' ], - languageId: mode.languageId, + languageId: autoClosingLanguageId, editorOpts: { autoSurround: 'brackets' } @@ -5362,11 +5278,9 @@ suite('autoClosingPairs', () => { viewModel.type('`', 'keyboard'); assert.strictEqual(model.getValue(), '(`) a = asd'); }); - mode.dispose(); }); - test('quote', () => { - let mode = new AutoClosingMode(); + test('autoClosingPairs - quote', () => { usingCursor({ text: [ 'var a = [];', @@ -5378,7 +5292,7 @@ suite('autoClosingPairs', () => { 'var g = (3+5);', 'var h = { a: \'value\' };', ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { let autoClosePositions = [ @@ -5393,13 +5307,13 @@ suite('autoClosingPairs', () => { ]; for (let i = 0, len = autoClosePositions.length; i < len; i++) { const lineNumber = i + 1; - const autoCloseColumns = extractSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); + const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { model.forceTokenization(lineNumber); - if (autoCloseColumns[column] === ColumnType.Special1) { + if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '\'', '\'\'', `auto closes @ (${lineNumber}, ${column})`); - } else if (autoCloseColumns[column] === ColumnType.Special2) { + } else if (autoCloseColumns[column] === AutoClosingColumnType.Special2) { assertType(editor, model, viewModel, lineNumber, column, '\'', '', `over types @ (${lineNumber}, ${column})`); } else { assertType(editor, model, viewModel, lineNumber, column, '\'', '\'', `does not auto close @ (${lineNumber}, ${column})`); @@ -5407,16 +5321,14 @@ suite('autoClosingPairs', () => { } } }); - mode.dispose(); }); - test('multi-character autoclose', () => { - let mode = new AutoClosingMode(); + test('autoClosingPairs - multi-character autoclose', () => { usingCursor({ text: [ '', ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { model.setValue('begi'); @@ -5429,32 +5341,26 @@ suite('autoClosingPairs', () => { viewModel.type('*', 'keyboard'); assert.strictEqual(model.getLineContent(1), '/** */'); }); - mode.dispose(); }); test('issue #72177: multi-character autoclose with conflicting patterns', () => { const languageId = 'autoClosingModeMultiChar'; - class AutoClosingModeMultiChar extends MockMode { - constructor() { - super(languageId); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - autoClosingPairs: [ - { open: '(', close: ')' }, - { open: '(*', close: '*)' }, - { open: '<@', close: '@>' }, - { open: '<@@', close: '@@>' }, - ], - })); - } - } - const mode = new AutoClosingModeMultiChar(); + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + autoClosingPairs: [ + { open: '(', close: ')' }, + { open: '(*', close: '*)' }, + { open: '<@', close: '@>' }, + { open: '<@@', close: '@@>' }, + ], + })); usingCursor({ text: [ '', ], - languageId: mode.languageId + languageId: languageId }, (editor, model, viewModel) => { viewModel.type('(', 'keyboard'); assert.strictEqual(model.getLineContent(1), '()'); @@ -5474,30 +5380,24 @@ suite('autoClosingPairs', () => { viewModel.type('(', 'keyboard'); assert.strictEqual(model.getLineContent(1), '<@@()@@>', `autocloses when before multi-character closing brace`); }); - mode.dispose(); }); test('issue #55314: Do not auto-close when ending with open', () => { const languageId = 'myElectricMode'; - class ElectricMode extends MockMode { - constructor() { - super(languageId); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - autoClosingPairs: [ - { open: '{', close: '}' }, - { open: '[', close: ']' }, - { open: '(', close: ')' }, - { open: '\'', close: '\'', notIn: ['string', 'comment'] }, - { open: '\"', close: '\"', notIn: ['string'] }, - { open: 'B\"', close: '\"', notIn: ['string', 'comment'] }, - { open: '`', close: '`', notIn: ['string', 'comment'] }, - { open: '/**', close: ' */', notIn: ['string'] } - ], - })); - } - } - const mode = new ElectricMode(); + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '\'', close: '\'', notIn: ['string', 'comment'] }, + { open: '\"', close: '\"', notIn: ['string'] }, + { open: 'B\"', close: '\"', notIn: ['string', 'comment'] }, + { open: '`', close: '`', notIn: ['string', 'comment'] }, + { open: '/**', close: ' */', notIn: ['string'] } + ], + })); usingCursor({ text: [ @@ -5506,7 +5406,7 @@ suite('autoClosingPairs', () => { 'little sheep', 'Big LAMB' ], - languageId: mode.languageId + languageId: languageId }, (editor, model, viewModel) => { model.forceTokenization(model.getLineCount()); assertType(editor, model, viewModel, 1, 4, '"', '"', `does not double quote when ending with open`); @@ -5519,29 +5419,25 @@ suite('autoClosingPairs', () => { model.forceTokenization(model.getLineCount()); assertType(editor, model, viewModel, 4, 3, '"', '"', `does not double quote when ending with open`); }); - mode.dispose(); }); test('issue #27937: Trying to add an item to the front of a list is cumbersome', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ 'var arr = ["b", "c"];' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { assertType(editor, model, viewModel, 1, 12, '"', '"', `does not over type and will not auto close`); }); - mode.dispose(); }); test('issue #25658 - Do not auto-close single/double quotes after word characters', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ '', ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { function typeCharacters(viewModel: ViewModel, chars: string): void { @@ -5598,17 +5494,15 @@ suite('autoClosingPairs', () => { typeCharacters(viewModel, 'teste"'); assert.strictEqual(model.getLineContent(8), 'teste"'); }); - mode.dispose(); }); test('issue #37315 - overtypes only those characters that it inserted', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ '', 'y=();' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5628,17 +5522,15 @@ suite('autoClosingPairs', () => { assert.strictEqual(model.getLineContent(2), 'y=());'); }); - mode.dispose(); }); test('issue #37315 - stops overtyping once cursor leaves area', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ '', 'y=();' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5649,17 +5541,15 @@ suite('autoClosingPairs', () => { viewModel.type(')', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=())'); }); - mode.dispose(); }); test('issue #37315 - it overtypes only once', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ '', 'y=();' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5673,17 +5563,15 @@ suite('autoClosingPairs', () => { viewModel.type(')', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=())'); }); - mode.dispose(); }); test('issue #37315 - it can remember multiple auto-closed instances', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ '', 'y=();' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5699,17 +5587,15 @@ suite('autoClosingPairs', () => { viewModel.type(')', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=(())'); }); - mode.dispose(); }); test('issue #118270 - auto closing deletes only those characters that it inserted', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ '', 'y=();' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5734,16 +5620,14 @@ suite('autoClosingPairs', () => { assert.strictEqual(model.getLineContent(2), 'y=);'); }); - mode.dispose(); }); test('issue #78527 - does not close quote on odd count', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ 'std::cout << \'"\' << entryMap' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [new Selection(1, 29, 1, 29)]); @@ -5762,61 +5646,55 @@ suite('autoClosingPairs', () => { viewModel.type(']', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'std::cout << \'"\' << entryMap["a"]'); }); - mode.dispose(); }); test('issue #85983 - editor.autoClosingBrackets: beforeWhitespace is incorrect for Python', () => { const languageId = 'pythonMode'; - class PythonMode extends MockMode { - constructor() { - super(languageId); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - autoClosingPairs: [ - { open: '{', close: '}' }, - { open: '[', close: ']' }, - { open: '(', close: ')' }, - { open: '\"', close: '\"', notIn: ['string'] }, - { open: 'r\"', close: '\"', notIn: ['string', 'comment'] }, - { open: 'R\"', close: '\"', notIn: ['string', 'comment'] }, - { open: 'u\"', close: '\"', notIn: ['string', 'comment'] }, - { open: 'U\"', close: '\"', notIn: ['string', 'comment'] }, - { open: 'f\"', close: '\"', notIn: ['string', 'comment'] }, - { open: 'F\"', close: '\"', notIn: ['string', 'comment'] }, - { open: 'b\"', close: '\"', notIn: ['string', 'comment'] }, - { open: 'B\"', close: '\"', notIn: ['string', 'comment'] }, - { open: '\'', close: '\'', notIn: ['string', 'comment'] }, - { open: 'r\'', close: '\'', notIn: ['string', 'comment'] }, - { open: 'R\'', close: '\'', notIn: ['string', 'comment'] }, - { open: 'u\'', close: '\'', notIn: ['string', 'comment'] }, - { open: 'U\'', close: '\'', notIn: ['string', 'comment'] }, - { open: 'f\'', close: '\'', notIn: ['string', 'comment'] }, - { open: 'F\'', close: '\'', notIn: ['string', 'comment'] }, - { open: 'b\'', close: '\'', notIn: ['string', 'comment'] }, - { open: 'B\'', close: '\'', notIn: ['string', 'comment'] }, - { open: '`', close: '`', notIn: ['string'] } - ], - })); - } - } - const mode = new PythonMode(); + + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '\"', close: '\"', notIn: ['string'] }, + { open: 'r\"', close: '\"', notIn: ['string', 'comment'] }, + { open: 'R\"', close: '\"', notIn: ['string', 'comment'] }, + { open: 'u\"', close: '\"', notIn: ['string', 'comment'] }, + { open: 'U\"', close: '\"', notIn: ['string', 'comment'] }, + { open: 'f\"', close: '\"', notIn: ['string', 'comment'] }, + { open: 'F\"', close: '\"', notIn: ['string', 'comment'] }, + { open: 'b\"', close: '\"', notIn: ['string', 'comment'] }, + { open: 'B\"', close: '\"', notIn: ['string', 'comment'] }, + { open: '\'', close: '\'', notIn: ['string', 'comment'] }, + { open: 'r\'', close: '\'', notIn: ['string', 'comment'] }, + { open: 'R\'', close: '\'', notIn: ['string', 'comment'] }, + { open: 'u\'', close: '\'', notIn: ['string', 'comment'] }, + { open: 'U\'', close: '\'', notIn: ['string', 'comment'] }, + { open: 'f\'', close: '\'', notIn: ['string', 'comment'] }, + { open: 'F\'', close: '\'', notIn: ['string', 'comment'] }, + { open: 'b\'', close: '\'', notIn: ['string', 'comment'] }, + { open: 'B\'', close: '\'', notIn: ['string', 'comment'] }, + { open: '`', close: '`', notIn: ['string'] } + ], + })); + usingCursor({ text: [ 'foo\'hello\'' ], - languageId: mode.languageId + languageId: languageId }, (editor, model, viewModel) => { assertType(editor, model, viewModel, 1, 4, '(', '(', `does not auto close @ (1, 4)`); }); - mode.dispose(); }); test('issue #78975 - Parentheses swallowing does not work when parentheses are inserted by autocomplete', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ '<div id' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [new Selection(1, 8, 1, 8)]); @@ -5829,17 +5707,15 @@ suite('autoClosingPairs', () => { viewModel.type('"', 'keyboard'); assert.strictEqual(model.getLineContent(1), '<div id="a"'); }); - mode.dispose(); }); test('issue #78833 - Add config to use old brackets/quotes overtyping', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ '', 'y=();' ], - languageId: mode.languageId, + languageId: autoClosingLanguageId, editorOpts: { autoClosingOvertype: 'always' } @@ -5860,15 +5736,13 @@ suite('autoClosingPairs', () => { viewModel.type(')', 'keyboard'); assert.strictEqual(model.getLineContent(2), 'y=();'); }); - mode.dispose(); }); test('issue #15825: accents on mac US intl keyboard', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5880,16 +5754,14 @@ suite('autoClosingPairs', () => { assert.strictEqual(model.getValue(), 'è'); }); - mode.dispose(); }); test('issue #90016: allow accents on mac US intl keyboard to surround selection', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ 'test' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [new Selection(1, 1, 1, 5)]); @@ -5902,16 +5774,14 @@ suite('autoClosingPairs', () => { assert.strictEqual(model.getValue(), '\'test\''); }); - mode.dispose(); }); test('issue #53357: Over typing ignores characters after backslash', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ 'console.log();' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [new Selection(1, 13, 1, 13)]); @@ -5928,16 +5798,14 @@ suite('autoClosingPairs', () => { viewModel.type('\'', 'keyboard'); assert.strictEqual(model.getValue(), 'console.log(\'it\\\'\');'); }); - mode.dispose(); }); test('issue #84998: Overtyping Brackets doesn\'t work after backslash', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ '' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [new Selection(1, 1, 1, 1)]); @@ -5957,17 +5825,15 @@ suite('autoClosingPairs', () => { viewModel.type(')', 'keyboard'); assert.strictEqual(model.getValue(), '\\(abc\\)'); }); - mode.dispose(); }); test('issue #2773: Accents (´`¨^, others?) are inserted in the wrong position (Mac)', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ 'hello', 'world' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -5983,16 +5849,14 @@ suite('autoClosingPairs', () => { assert.strictEqual(model.getValue(), '`hello\nworld'); assertCursor(viewModel, new Selection(1, 2, 2, 2)); }); - mode.dispose(); }); test('issue #26820: auto close quotes when not used as accents', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ '' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -6050,16 +5914,14 @@ suite('autoClosingPairs', () => { assert.strictEqual(model.getValue(), 'abc\''); }); - mode.dispose(); }); test('issue #144690: Quotes do not overtype when using US Intl PC keyboard layout', () => { - const mode = new AutoClosingMode(); usingCursor({ text: [ '' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { assertCursor(viewModel, new Position(1, 1)); @@ -6078,16 +5940,14 @@ suite('autoClosingPairs', () => { assert.strictEqual(model.getValue(), `'';`); }); - mode.dispose(); }); test('issue #144693: Typing a quote using US Intl PC keyboard layout always surrounds words', () => { - const mode = new AutoClosingMode(); usingCursor({ text: [ 'const hello = 3;' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [new Selection(1, 7, 1, 12)]); @@ -6101,16 +5961,14 @@ suite('autoClosingPairs', () => { assert.strictEqual(model.getValue(), `const é = 3;`); }); - mode.dispose(); }); test('issue #82701: auto close does not execute when IME is canceled via backspace', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ '{}' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [new Selection(1, 2, 1, 2)]); @@ -6121,16 +5979,14 @@ suite('autoClosingPairs', () => { viewModel.endComposition('keyboard'); assert.strictEqual(model.getValue(), '{}'); }); - mode.dispose(); }); test('issue #20891: All cursors should do the same thing', () => { - let mode = new AutoClosingMode(); usingCursor({ text: [ 'var a = asd' ], - languageId: mode.languageId + languageId: autoClosingLanguageId }, (editor, model, viewModel) => { viewModel.setSelections('test', [ @@ -6143,24 +5999,19 @@ suite('autoClosingPairs', () => { assert.strictEqual(model.getValue(), 'var a = `asd`'); }); - mode.dispose(); }); test('issue #41825: Special handling of quotes in surrounding pairs', () => { const languageId = 'myMode'; - class MyMode extends MockMode { - constructor() { - super(languageId); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - surroundingPairs: [ - { open: '"', close: '"' }, - { open: '\'', close: '\'' }, - ] - })); - } - } - const mode = new MyMode(); + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + surroundingPairs: [ + { open: '"', close: '"' }, + { open: '\'', close: '\'' }, + ] + })); + const model = createTextModel('var x = \'hi\';', languageId); withTestCodeEditor(model, {}, (editor, viewModel) => { @@ -6178,18 +6029,14 @@ suite('autoClosingPairs', () => { viewModel.type('\'', 'keyboard'); assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'var x = \'hi\';', 'assert2'); }); - - model.dispose(); - mode.dispose(); }); test('All cursors should do the same thing when deleting left', () => { - let mode = new AutoClosingMode(); let model = createTextModel( [ 'var a = ()' ].join('\n'), - mode.languageId + autoClosingLanguageId ); withTestCodeEditor(model, {}, (editor, viewModel) => { @@ -6203,8 +6050,6 @@ suite('autoClosingPairs', () => { assert.strictEqual(model.getValue(), 'va a = )'); }); - model.dispose(); - mode.dispose(); }); test('issue #7100: Mouse word selection is strange when non-word character is at the end of line', () => { @@ -6232,8 +6077,6 @@ suite('autoClosingPairs', () => { }); assertCursor(viewModel, new Selection(3, 7, 4, 7)); }); - - model.dispose(); }); }); diff --git a/src/vs/editor/test/browser/testCommand.ts b/src/vs/editor/test/browser/testCommand.ts index bb397f60135..22d6a6fc9f2 100644 --- a/src/vs/editor/test/browser/testCommand.ts +++ b/src/vs/editor/test/browser/testCommand.ts @@ -18,7 +18,7 @@ export function testCommand( lines: string[], languageId: string | null, selection: Selection, - commandFactory: (selection: Selection) => ICommand, + commandFactory: (accessor: ServicesAccessor, selection: Selection) => ICommand, expectedLines: string[], expectedSelection: Selection, forceTokenization?: boolean, @@ -29,7 +29,7 @@ export function testCommand( if (prepare) { instantiationService.invokeFunction(prepare, disposables); } - const model = instantiateTextModel(instantiationService, lines.join('\n'), languageId); + const model = disposables.add(instantiateTextModel(instantiationService, lines.join('\n'), languageId)); const editor = disposables.add(instantiateTestCodeEditor(instantiationService, model)); const viewModel = editor.getViewModel()!; @@ -39,7 +39,8 @@ export function testCommand( viewModel.setSelections('tests', [selection]); - viewModel.executeCommand(commandFactory(viewModel.getSelection()), 'tests'); + const command = instantiationService.invokeFunction((accessor) => commandFactory(accessor, viewModel.getSelection())); + viewModel.executeCommand(command, 'tests'); assert.deepStrictEqual(model.getLinesContent(), expectedLines); diff --git a/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts b/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts index 3267ae8af50..878ac72db23 100644 --- a/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts +++ b/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts @@ -8,8 +8,8 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { Selection } from 'vs/editor/common/core/selection'; import { Range } from 'vs/editor/common/core/range'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageService } from 'vs/editor/common/languages/language'; suite('CodeEditorWidget', () => { @@ -31,9 +31,10 @@ suite('CodeEditorWidget', () => { }); test('onDidChangeModelLanguage', () => { - withTestCodeEditor('', {}, (editor, viewModel) => { + withTestCodeEditor('', {}, (editor, viewModel, instantiationService) => { + const languageService = instantiationService.get(ILanguageService); const disposables = new DisposableStore(); - disposables.add(ModesRegistry.registerLanguage({ id: 'testMode' })); + disposables.add(languageService.registerLanguage({ id: 'testMode' })); let invoked = false; disposables.add(editor.onDidChangeModelLanguage((e) => { @@ -50,8 +51,10 @@ suite('CodeEditorWidget', () => { test('onDidChangeModelLanguageConfiguration', () => { withTestCodeEditor('', {}, (editor, viewModel, instantiationService) => { + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const languageService = instantiationService.get(ILanguageService); const disposables = new DisposableStore(); - disposables.add(ModesRegistry.registerLanguage({ id: 'testMode' })); + disposables.add(languageService.registerLanguage({ id: 'testMode' })); viewModel.model.setMode('testMode'); let invoked = false; @@ -59,7 +62,7 @@ suite('CodeEditorWidget', () => { invoked = true; })); - disposables.add(LanguageConfigurationRegistry.register('testMode', { + disposables.add(languageConfigurationService.register('testMode', { brackets: [['(', ')']] })); @@ -151,4 +154,67 @@ suite('CodeEditorWidget', () => { }); }); + test('issue #146174: Events delivered out of order when adding decorations in content change listener (1 of 2)', () => { + withTestCodeEditor('', {}, (editor, viewModel) => { + const disposables = new DisposableStore(); + + const calls: string[] = []; + disposables.add(editor.onDidChangeModelContent((e) => { + calls.push(`listener1 - contentchange(${e.changes.reduce<any[]>((aggr, c) => [...aggr, c.text, c.rangeOffset, c.rangeLength], []).join(', ')})`); + })); + disposables.add(editor.onDidChangeCursorSelection((e) => { + calls.push(`listener1 - cursorchange(${e.selection.positionLineNumber}, ${e.selection.positionColumn})`); + })); + disposables.add(editor.onDidChangeModelContent((e) => { + calls.push(`listener2 - contentchange(${e.changes.reduce<any[]>((aggr, c) => [...aggr, c.text, c.rangeOffset, c.rangeLength], []).join(', ')})`); + })); + disposables.add(editor.onDidChangeCursorSelection((e) => { + calls.push(`listener2 - cursorchange(${e.selection.positionLineNumber}, ${e.selection.positionColumn})`); + })); + + viewModel.type('a', 'test'); + + assert.deepStrictEqual(calls, ([ + 'listener1 - contentchange(a, 0, 0)', + 'listener2 - contentchange(a, 0, 0)', + 'listener1 - cursorchange(1, 2)', + 'listener2 - cursorchange(1, 2)', + ])); + + disposables.dispose(); + }); + }); + + test('issue #146174: Events delivered out of order when adding decorations in content change listener (2 of 2)', () => { + withTestCodeEditor('', {}, (editor, viewModel) => { + const disposables = new DisposableStore(); + + const calls: string[] = []; + disposables.add(editor.onDidChangeModelContent((e) => { + calls.push(`listener1 - contentchange(${e.changes.reduce<any[]>((aggr, c) => [...aggr, c.text, c.rangeOffset, c.rangeLength], []).join(', ')})`); + editor.deltaDecorations([], [{ range: new Range(1, 1, 1, 1), options: { description: 'test' } }]); + })); + disposables.add(editor.onDidChangeCursorSelection((e) => { + calls.push(`listener1 - cursorchange(${e.selection.positionLineNumber}, ${e.selection.positionColumn})`); + })); + disposables.add(editor.onDidChangeModelContent((e) => { + calls.push(`listener2 - contentchange(${e.changes.reduce<any[]>((aggr, c) => [...aggr, c.text, c.rangeOffset, c.rangeLength], []).join(', ')})`); + })); + disposables.add(editor.onDidChangeCursorSelection((e) => { + calls.push(`listener2 - cursorchange(${e.selection.positionLineNumber}, ${e.selection.positionColumn})`); + })); + + viewModel.type('a', 'test'); + + assert.deepStrictEqual(calls, ([ + 'listener1 - contentchange(a, 0, 0)', + 'listener2 - contentchange(a, 0, 0)', + 'listener1 - cursorchange(1, 2)', + 'listener2 - cursorchange(1, 2)', + ])); + + disposables.dispose(); + }); + }); + }); diff --git a/src/vs/editor/test/common/commentMode.ts b/src/vs/editor/test/common/commentMode.ts deleted file mode 100644 index 07828551045..00000000000 --- a/src/vs/editor/test/common/commentMode.ts +++ /dev/null @@ -1,19 +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 { CommentRule } from 'vs/editor/common/languages/languageConfiguration'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; - -export class CommentMode extends MockMode { - public static readonly id = 'commentMode'; - - constructor(commentsConfig: CommentRule) { - super(CommentMode.id); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - comments: commentsConfig - })); - } -} diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts index d6499ea45fb..16c59a7bfe2 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/brackets.test.ts @@ -8,7 +8,6 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { LanguageAgnosticBracketTokens } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets'; import { SmallImmutableSet, DenseKeyProvider } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/smallImmutableSet'; import { Token, TokenKind } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; suite('Bracket Pair Colorizer - Brackets', () => { @@ -25,7 +24,8 @@ suite('Bracket Pair Colorizer - Brackets', () => { }; const disposableStore = new DisposableStore(); - disposableStore.add(LanguageConfigurationRegistry.register(languageId, { + const languageConfigService = new TestLanguageConfigurationService(); + disposableStore.add(languageConfigService.register(languageId, { brackets: [ ['{', '}'], ['[', ']'], ['(', ')'], ['begin', 'end'], ['case', 'endcase'], ['casez', 'endcase'], // Verilog @@ -34,8 +34,7 @@ suite('Bracket Pair Colorizer - Brackets', () => { ] })); - const languageConfigService = new TestLanguageConfigurationService(); - const brackets = new LanguageAgnosticBracketTokens(denseKeyProvider, l => languageConfigService.getLanguageConfiguration(l, undefined)); + const brackets = new LanguageAgnosticBracketTokens(denseKeyProvider, l => languageConfigService.getLanguageConfiguration(l)); const bracketsExpected = [ { text: '{', length: 1, kind: 'OpeningBracket', bracketId: getKey('{'), bracketIds: getImmutableSet(['{']) }, { text: '[', length: 1, kind: 'OpeningBracket', bracketId: getKey('['), bracketIds: getImmutableSet(['[']) }, diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts index 70180f74827..2166aff6c8e 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts @@ -4,33 +4,35 @@ *--------------------------------------------------------------------------------------------*/ import assert = require('assert'); -import { Disposable, disposeOnReturn } from 'vs/base/common/lifecycle'; +import { DisposableStore, disposeOnReturn } from 'vs/base/common/lifecycle'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { BracketPairInfo } from 'vs/editor/common/textModelBracketPairs'; -import { LanguageConfiguration } from 'vs/editor/common/languages/languageConfiguration'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { createModelServices, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; +import { TextModel } from 'vs/editor/common/model/textModel'; suite('Bracket Pair Colorizer - getBracketPairsInRange', () => { - function createLang() { - return MockLanguage.create({ - configuration: { - colorizedBracketPairs: [ - ['{', '}'], - ['[', ']'], - ['(', ')'], - ] - }, - }); + + function createTextModelWithColorizedBracketPairs(store: DisposableStore, text: string): TextModel { + const languageId = 'testLanguage'; + const instantiationService = createModelServices(store); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + + store.add(languageConfigurationService.register(languageId, { + colorizedBracketPairs: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ] + })); + return store.add(instantiateTextModel(instantiationService, text, languageId)); } test('Basic 1', () => { disposeOnReturn(store => { const doc = new AnnotatedDocument(`{ ( [] ¹ ) [ ² { } ] () } []`); - const model = store.add( - createTextModel(doc.text, store.add(createLang()).id) - ); + const model = createTextModelWithColorizedBracketPairs(store, doc.text); assert.deepStrictEqual( model.bracketPairs .getBracketPairsInRange(doc.range(1, 2)) @@ -62,9 +64,7 @@ suite('Bracket Pair Colorizer - getBracketPairsInRange', () => { test('Basic 2', () => { disposeOnReturn(store => { const doc = new AnnotatedDocument(`{ ( [] ¹ ²) [ { } ] () } []`); - const model = store.add( - createTextModel(doc.text, store.add(createLang()).id) - ); + const model = createTextModelWithColorizedBracketPairs(store, doc.text); assert.deepStrictEqual( model.bracketPairs .getBracketPairsInRange(doc.range(1, 2)) @@ -90,9 +90,7 @@ suite('Bracket Pair Colorizer - getBracketPairsInRange', () => { test('Basic Empty', () => { disposeOnReturn(store => { const doc = new AnnotatedDocument(`¹ ² { ( [] ) [ { } ] () } []`); - const model = store.add( - createTextModel(doc.text, store.add(createLang()).id) - ); + const model = createTextModelWithColorizedBracketPairs(store, doc.text); assert.deepStrictEqual( model.bracketPairs .getBracketPairsInRange(doc.range(1, 2)) @@ -105,9 +103,7 @@ suite('Bracket Pair Colorizer - getBracketPairsInRange', () => { test('Basic All', () => { disposeOnReturn(store => { const doc = new AnnotatedDocument(`¹ { ( [] ) [ { } ] () } [] ²`); - const model = store.add( - createTextModel(doc.text, store.add(createLang()).id) - ); + const model = createTextModelWithColorizedBracketPairs(store, doc.text); assert.deepStrictEqual( model.bracketPairs .getBracketPairsInRange(doc.range(1, 2)) @@ -228,28 +224,3 @@ class AnnotatedDocument { return Range.fromPositions(this.positions.get(start)!, this.positions.get(end)!); } } - -interface MockLanguageOptions { - configuration?: LanguageConfiguration; -} - -class MockLanguage extends Disposable { - private static id = 0; - - public static create(options: MockLanguageOptions) { - const id = `lang${this.id++}`; - - return new MockLanguage(id, options); - } - - constructor( - public readonly id: string, - options: MockLanguageOptions - ) { - super(); - - if (options.configuration) { - this._register(LanguageConfigurationRegistry.register(id, options.configuration)); - } - } -} 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 f465597af2f..6871809fb7b 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts @@ -11,19 +11,18 @@ import { DenseKeyProvider } from 'vs/editor/common/model/bracketPairsTextModelPa import { TextBufferTokenizer, Token, Tokenizer, TokenKind } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer'; import { TextModel } from 'vs/editor/common/model/textModel'; import { EncodedTokenizationResult, IState, ITokenizationSupport, LanguageId, MetadataConsts, StandardTokenType, TokenizationRegistry } from 'vs/editor/common/languages'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { createModelServices, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; -import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; suite('Bracket Pair Colorizer - Tokenizer', () => { test('Basic', () => { const mode1 = 'testMode1'; const disposableStore = new DisposableStore(); const instantiationService = createModelServices(disposableStore); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); const languageService = instantiationService.get(ILanguageService); - disposableStore.add(ModesRegistry.registerLanguage({ id: mode1 })); + disposableStore.add(languageService.registerLanguage({ id: mode1 })); const encodedMode1 = languageService.languageIdCodec.encodeLanguageId(mode1); const denseKeyProvider = new DenseKeyProvider<string>(); @@ -36,15 +35,14 @@ suite('Bracket Pair Colorizer - Tokenizer', () => { ]); disposableStore.add(TokenizationRegistry.register(mode1, document.getTokenizationSupport())); - disposableStore.add(LanguageConfigurationRegistry.register(mode1, { + disposableStore.add(languageConfigurationService.register(mode1, { brackets: [['{', '}'], ['[', ']'], ['(', ')'], ['begin', 'end']], })); const model = disposableStore.add(instantiateTextModel(instantiationService, document.getText(), mode1)); model.forceTokenization(model.getLineCount()); - const languageConfigService = new TestLanguageConfigurationService(); - const brackets = new LanguageAgnosticBracketTokens(denseKeyProvider, l => languageConfigService.getLanguageConfiguration(l, undefined)); + const brackets = new LanguageAgnosticBracketTokens(denseKeyProvider, l => languageConfigurationService.getLanguageConfiguration(l)); const tokens = readAllTokens(new TextBufferTokenizer(model, brackets)); diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index d6f63e3b69c..1b1f31881be 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -11,7 +11,7 @@ import { Range } from 'vs/editor/common/core/range'; import { TextModel } from 'vs/editor/common/model/textModel'; import { InternalModelContentChangeEvent, ModelRawContentChangedEvent, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/textModelEvents'; import { EncodedTokenizationResult, IState, MetadataConsts, TokenizationRegistry } from 'vs/editor/common/languages'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { NullState } from 'vs/editor/common/languages/nullTokenize'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { createModelServices, createTextModel, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; @@ -383,12 +383,13 @@ suite('Editor Model - Words', () => { class OuterMode extends MockMode { constructor( - @ILanguageService languageService: ILanguageService + @ILanguageService languageService: ILanguageService, + @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService ) { super(OUTER_LANGUAGE_ID); const languageIdCodec = languageService.languageIdCodec; - this._register(LanguageConfigurationRegistry.register(this.languageId, {})); + this._register(languageConfigurationService.register(this.languageId, {})); this._register(TokenizationRegistry.register(this.languageId, { getInitialState: (): IState => NullState, @@ -417,9 +418,11 @@ suite('Editor Model - Words', () => { } class InnerMode extends MockMode { - constructor() { + constructor( + @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService + ) { super(INNER_LANGUAGE_ID); - this._register(LanguageConfigurationRegistry.register(this.languageId, {})); + this._register(languageConfigurationService.register(this.languageId, {})); } } @@ -456,7 +459,7 @@ suite('Editor Model - Words', () => { const disposables = new DisposableStore(); const instantiationService = createModelServices(disposables); const outerMode = disposables.add(instantiationService.createInstance(OuterMode)); - disposables.add(new InnerMode()); + disposables.add(instantiationService.createInstance(InnerMode)); const model = disposables.add(instantiateTextModel(instantiationService, 'ab<xx>ab<x>', outerMode.languageId)); @@ -473,19 +476,17 @@ suite('Editor Model - Words', () => { test('issue #61296: VS code freezes when editing CSS file with emoji', () => { const MODE_ID = 'testMode'; + const disposables = new DisposableStore(); + const instantiationService = createModelServices(disposables); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const languageService = instantiationService.get(ILanguageService); - const mode = new class extends MockMode { - constructor() { - super(MODE_ID); - this._register(LanguageConfigurationRegistry.register(this.languageId, { - wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g - })); - } - }; - disposables.push(mode); + disposables.add(languageService.registerLanguage({ id: MODE_ID })); + disposables.add(languageConfigurationService.register(MODE_ID, { + wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g + })); - const thisModel = createTextModel('.🐷-a-b', MODE_ID); - disposables.push(thisModel); + 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 }); @@ -495,5 +496,7 @@ suite('Editor Model - Words', () => { 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 }); + + 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 7e9b758974d..59d3450eedb 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -11,12 +11,24 @@ import { IFoundBracket } from 'vs/editor/common/textModelBracketPairs'; import { TextModel } from 'vs/editor/common/model/textModel'; import { ITokenizationSupport, MetadataConsts, TokenizationRegistry, StandardTokenType, EncodedTokenizationResult } from 'vs/editor/common/languages'; import { CharacterPair } from 'vs/editor/common/languages/languageConfiguration'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { NullState } from 'vs/editor/common/languages/nullTokenize'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { TestLineToken } from 'vs/editor/test/common/core/testLineToken'; import { createModelServices, createTextModel, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; + +function createTextModelWithBrackets(disposables: DisposableStore, text: string, brackets: CharacterPair[]): TextModel { + const languageId = 'bracketMode2'; + const instantiationService = createModelServices(disposables); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const languageService = instantiationService.get(ILanguageService); + + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { brackets })); + + return disposables.add(instantiateTextModel(instantiationService, text, languageId)); +} suite('TextModelWithTokens', () => { @@ -70,16 +82,16 @@ suite('TextModelWithTokens', () => { const languageId = 'testMode'; const disposables = new DisposableStore(); + const instantiationService = createModelServices(disposables); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const languageService = instantiationService.get(ILanguageService); - disposables.add(ModesRegistry.registerLanguage({ id: languageId })); - disposables.add(LanguageConfigurationRegistry.register(languageId, { + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { brackets: brackets })); - const model = disposables.add(createTextModel( - contents.join('\n'), - languageId - )); + const model = disposables.add(instantiateTextModel(instantiationService, contents.join('\n'), languageId)); // findPrevBracket { @@ -161,11 +173,17 @@ suite('TextModelWithTokens - bracket matching', () => { const languageId = 'bracketMode1'; let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let languageConfigurationService: ILanguageConfigurationService; + let languageService: ILanguageService; setup(() => { disposables = new DisposableStore(); - disposables.add(ModesRegistry.registerLanguage({ id: languageId })); - disposables.add(LanguageConfigurationRegistry.register(languageId, { + instantiationService = createModelServices(disposables); + languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + languageService = instantiationService.get(ILanguageService); + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { brackets: [ ['{', '}'], ['[', ']'], @@ -179,10 +197,10 @@ suite('TextModelWithTokens - bracket matching', () => { }); test('bracket matching 1', () => { - let text = + const text = ')]}{[(' + '\n' + ')]}{[('; - let model = createTextModel(text, languageId); + const model = disposables.add(instantiateTextModel(instantiationService, text, languageId)); assertIsNotBracket(model, 1, 1); assertIsNotBracket(model, 1, 2); @@ -199,20 +217,18 @@ suite('TextModelWithTokens - bracket matching', () => { assertIsNotBracket(model, 2, 5); assertIsNotBracket(model, 2, 6); assertIsNotBracket(model, 2, 7); - - model.dispose(); }); test('bracket matching 2', () => { - let text = + const text = 'var bar = {' + '\n' + 'foo: {' + '\n' + '}, bar: {hallo: [{' + '\n' + '}, {' + '\n' + '}]}}'; - let model = createTextModel(text, languageId); + const model = disposables.add(instantiateTextModel(instantiationService, text, languageId)); - let brackets: [Position, Range, Range][] = [ + const brackets: [Position, Range, Range][] = [ [new Position(1, 11), new Range(1, 11, 1, 12), new Range(5, 4, 5, 5)], [new Position(1, 12), new Range(1, 11, 1, 12), new Range(5, 4, 5, 5)], @@ -254,26 +270,12 @@ suite('TextModelWithTokens - bracket matching', () => { } } } - - model.dispose(); }); }); suite('TextModelWithTokens', () => { test('bracket matching 3', () => { - - const languageId = 'bracketMode2'; - const disposables = new DisposableStore(); - disposables.add(ModesRegistry.registerLanguage({ id: languageId })); - disposables.add(LanguageConfigurationRegistry.register(languageId, { - brackets: [ - ['if', 'end if'], - ['loop', 'end loop'], - ['begin', 'end'] - ], - })); - const text = [ 'begin', ' loop', @@ -290,7 +292,12 @@ suite('TextModelWithTokens', () => { 'end;', ].join('\n'); - const model = disposables.add(createTextModel(text, languageId)); + const disposables = new DisposableStore(); + const model = createTextModelWithBrackets(disposables, text, [ + ['if', 'end if'], + ['loop', 'end loop'], + ['begin', 'end'] + ]); // <if> ... <end ifa> is not matched assertIsNotBracket(model, 10, 9); @@ -311,17 +318,6 @@ suite('TextModelWithTokens', () => { }); test('bracket matching 4', () => { - - const languageId = 'bracketMode2'; - const disposables = new DisposableStore(); - disposables.add(ModesRegistry.registerLanguage({ id: languageId })); - disposables.add(LanguageConfigurationRegistry.register(languageId, { - brackets: [ - ['recordbegin', 'endrecord'], - ['simplerecordbegin', 'endrecord'], - ], - })); - const text = [ 'recordbegin', ' simplerecordbegin', @@ -329,7 +325,11 @@ suite('TextModelWithTokens', () => { 'endrecord', ].join('\n'); - const model = disposables.add(createTextModel(text, languageId)); + const disposables = new DisposableStore(); + const model = createTextModelWithBrackets(disposables, text, [ + ['recordbegin', 'endrecord'], + ['simplerecordbegin', 'endrecord'], + ]); // <recordbegin> ... <endrecord> is matched assertIsBracket(model, new Position(1, 1), [new Range(1, 1, 1, 12), new Range(4, 1, 4, 10)]); @@ -345,13 +345,15 @@ suite('TextModelWithTokens', () => { test('issue #95843: Highlighting of closing braces is indicating wrong brace when cursor is behind opening brace', () => { const disposables = new DisposableStore(); const instantiationService = createModelServices(disposables); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const languageService = instantiationService.get(ILanguageService); const mode1 = 'testMode1'; const mode2 = 'testMode2'; - const languageIdCodec = instantiationService.get(ILanguageService).languageIdCodec; + const languageIdCodec = languageService.languageIdCodec; - disposables.add(ModesRegistry.registerLanguage({ id: mode1 })); - disposables.add(ModesRegistry.registerLanguage({ id: mode2 })); + disposables.add(languageService.registerLanguage({ id: mode1 })); + disposables.add(languageService.registerLanguage({ id: mode2 })); const encodedMode1 = languageIdCodec!.encodeLanguageId(mode1); const encodedMode2 = languageIdCodec!.encodeLanguageId(mode2); @@ -411,14 +413,14 @@ suite('TextModelWithTokens', () => { }; disposables.add(TokenizationRegistry.register(mode1, tokenizationSupport)); - disposables.add(LanguageConfigurationRegistry.register(mode1, { + disposables.add(languageConfigurationService.register(mode1, { brackets: [ ['{', '}'], ['[', ']'], ['(', ')'] ], })); - disposables.add(LanguageConfigurationRegistry.register(mode2, { + disposables.add(languageConfigurationService.register(mode2, { brackets: [ ['{', '}'], ['[', ']'], @@ -448,6 +450,7 @@ suite('TextModelWithTokens', () => { test('issue #88075: TypeScript brace matching is incorrect in `${}` strings', () => { const disposables = new DisposableStore(); const instantiationService = createModelServices(disposables); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); const mode = 'testMode'; const languageIdCodec = instantiationService.get(ILanguageService).languageIdCodec; @@ -496,7 +499,7 @@ suite('TextModelWithTokens', () => { }; disposables.add(TokenizationRegistry.register(mode, tokenizationSupport)); - disposables.add(LanguageConfigurationRegistry.register(mode, { + disposables.add(languageConfigurationService.register(mode, { brackets: [ ['{', '}'], ['[', ']'], @@ -605,28 +608,25 @@ suite('TextModelWithTokens regression tests', () => { test('microsoft/monaco-editor#133: Error: Cannot read property \'modeId\' of undefined', () => { - const languageId = 'testMode'; - const disposables = new DisposableStore(); - disposables.add(ModesRegistry.registerLanguage({ id: languageId })); - disposables.add(LanguageConfigurationRegistry.register(languageId, { - brackets: [ + const model = createTextModelWithBrackets( + disposables, + [ + 'Imports System', + 'Imports System.Collections.Generic', + '', + 'Module m1', + '', + '\tSub Main()', + '\tEnd Sub', + '', + 'End Module', + ].join('\n'), + [ ['module', 'end module'], ['sub', 'end sub'] ] - })); - - const model = disposables.add(createTextModel([ - 'Imports System', - 'Imports System.Collections.Generic', - '', - 'Module m1', - '', - '\tSub Main()', - '\tEnd Sub', - '', - 'End Module', - ].join('\n'), languageId)); + ); const actual = model.bracketPairs.matchBracket(new Position(4, 1)); assert.deepStrictEqual(actual, [new Range(4, 1, 4, 7), new Range(9, 1, 9, 11)]); @@ -636,22 +636,20 @@ suite('TextModelWithTokens regression tests', () => { test('issue #11856: Bracket matching does not work as expected if the opening brace symbol is contained in the closing brace symbol', () => { - const languageId = 'testMode'; const disposables = new DisposableStore(); - disposables.add(ModesRegistry.registerLanguage({ id: languageId })); - disposables.add(LanguageConfigurationRegistry.register(languageId, { - brackets: [ + const model = createTextModelWithBrackets( + disposables, + [ + 'sequence "outer"', + ' sequence "inner"', + ' endsequence', + 'endsequence', + ].join('\n'), + [ ['sequence', 'endsequence'], ['feature', 'endfeature'] ] - })); - - const model = disposables.add(createTextModel([ - 'sequence "outer"', - ' sequence "inner"', - ' endsequence', - 'endsequence', - ].join('\n'), languageId)); + ); const actual = model.bracketPairs.matchBracket(new Position(3, 9)); assert.deepStrictEqual(actual, [new Range(3, 6, 3, 17), new Range(2, 6, 2, 14)]); @@ -662,12 +660,13 @@ suite('TextModelWithTokens regression tests', () => { test('issue #63822: Wrong embedded language detected for empty lines', () => { const disposables = new DisposableStore(); const instantiationService = createModelServices(disposables); + const languageService = instantiationService.get(ILanguageService); const outerMode = 'outerMode'; const innerMode = 'innerMode'; - disposables.add(ModesRegistry.registerLanguage({ id: outerMode })); - disposables.add(ModesRegistry.registerLanguage({ id: innerMode })); + disposables.add(languageService.registerLanguage({ id: outerMode })); + disposables.add(languageService.registerLanguage({ id: innerMode })); const languageIdCodec = instantiationService.get(ILanguageService).languageIdCodec; const encodedInnerMode = languageIdCodec.encodeLanguageId(innerMode); @@ -699,10 +698,13 @@ suite('TextModelWithTokens regression tests', () => { suite('TextModel.getLineIndentGuide', () => { function assertIndentGuides(lines: [number, number, number, number, string][], tabSize: number): void { const languageId = 'testLang'; - const registration = ModesRegistry.registerLanguage({ id: languageId }); + const disposables = new DisposableStore(); + const instantiationService = createModelServices(disposables); + const languageService = instantiationService.get(ILanguageService); + disposables.add(languageService.registerLanguage({ id: languageId })); let text = lines.map(l => l[4]).join('\n'); - let model = createTextModel(text, languageId); + let model = disposables.add(instantiateTextModel(instantiationService, text, languageId)); model.updateOptions({ tabSize: tabSize }); let actualIndents = model.guides.getLinesIndentGuides(1, model.getLineCount()); @@ -715,8 +717,7 @@ suite('TextModel.getLineIndentGuide', () => { assert.deepStrictEqual(actual, lines); - model.dispose(); - registration.dispose(); + disposables.dispose(); } test('getLineIndentGuide one level 2', () => { diff --git a/src/vs/editor/test/common/modes/languageConfiguration.test.ts b/src/vs/editor/test/common/modes/languageConfiguration.test.ts index b6afdf79088..49f076e217e 100644 --- a/src/vs/editor/test/common/modes/languageConfiguration.test.ts +++ b/src/vs/editor/test/common/modes/languageConfiguration.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { StandardTokenType } from 'vs/editor/common/languages'; import { StandardAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; suite('StandardAutoClosingPairConditional', () => { @@ -91,10 +91,11 @@ suite('StandardAutoClosingPairConditional', () => { }); test('language configurations priorities', () => { + const languageConfigurationService = new TestLanguageConfigurationService(); const id = 'testLang1'; - const d1 = LanguageConfigurationRegistry.register(id, { comments: { lineComment: '1' } }, 100); - const d2 = LanguageConfigurationRegistry.register(id, { comments: { lineComment: '2' } }, 10); - assert.strictEqual(LanguageConfigurationRegistry.getComments(id)?.lineCommentToken, '1'); + const d1 = languageConfigurationService.register(id, { comments: { lineComment: '1' } }, 100); + const d2 = languageConfigurationService.register(id, { comments: { lineComment: '2' } }, 10); + assert.strictEqual(languageConfigurationService.getLanguageConfiguration(id).comments?.lineCommentToken, '1'); d1.dispose(); d2.dispose(); }); diff --git a/src/vs/editor/test/common/modes/testLanguageConfigurationService.ts b/src/vs/editor/test/common/modes/testLanguageConfigurationService.ts index 57f0b7713c1..ece0981cbd7 100644 --- a/src/vs/editor/test/common/modes/testLanguageConfigurationService.ts +++ b/src/vs/editor/test/common/modes/testLanguageConfigurationService.ts @@ -3,30 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { LanguageConfiguration } from 'vs/editor/common/languages/languageConfiguration'; import { ILanguageConfigurationService, LanguageConfigurationRegistry, LanguageConfigurationServiceChangeEvent, ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; -export class TestLanguageConfigurationService implements ILanguageConfigurationService { +export class TestLanguageConfigurationService extends Disposable implements ILanguageConfigurationService { _serviceBrand: undefined; - private registration: IDisposable | undefined = undefined; + private readonly _registry = this._register(new LanguageConfigurationRegistry()); - private readonly onDidChangeEmitter = new Emitter<LanguageConfigurationServiceChangeEvent>({ - onFirstListenerAdd: () => { - this.registration = LanguageConfigurationRegistry.onDidChange((e) => { - this.onDidChangeEmitter.fire(new LanguageConfigurationServiceChangeEvent(e.languageId)); - }); - }, - onLastListenerRemove: () => { - this.registration?.dispose(); - this.registration = undefined; - } - }); - public readonly onDidChange = this.onDidChangeEmitter.event; + private readonly _onDidChange = this._register(new Emitter<LanguageConfigurationServiceChangeEvent>()); + public readonly onDidChange = this._onDidChange.event; - getLanguageConfiguration(languageId: string, resource?: URI): ResolvedLanguageConfiguration { - return LanguageConfigurationRegistry.getLanguageConfiguration(languageId) ?? + constructor() { + super(); + this._register(this._registry.onDidChange((e) => this._onDidChange.fire(new LanguageConfigurationServiceChangeEvent(e.languageId)))); + } + + register(languageId: string, configuration: LanguageConfiguration, priority?: number): IDisposable { + return this._registry.register(languageId, configuration, priority); + } + + getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration { + return this._registry.getLanguageConfiguration(languageId) ?? new ResolvedLanguageConfiguration('unknown', {}); } } diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index 7d89dddc014..6f5b396e05e 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -20,14 +20,13 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { createModelServices, createTextModel } from 'vs/editor/test/common/testTextModel'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits, SemanticTokensLegend } from 'vs/editor/common/languages'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Barrier, timeout } from 'vs/base/common/async'; import { LanguageService } from 'vs/editor/common/services/languageService'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; @@ -37,32 +36,30 @@ import { LanguageFeatureDebounceService } from 'vs/editor/common/services/langua import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; const GENERATE_TESTS = false; suite('ModelService', () => { let disposables: DisposableStore; - let modelService: ModelService; + let modelService: IModelService; + let instantiationService: TestInstantiationService; setup(() => { disposables = new DisposableStore(); + const configService = new TestConfigurationService(); configService.setUserConfiguration('files', { 'eol': '\n' }); configService.setUserConfiguration('files', { 'eol': '\r\n' }, URI.file(platform.isWindows ? 'c:\\myroot' : '/myroot')); - const dialogService = new TestDialogService(); - const logService = new NullLogService(); - modelService = disposables.add(new ModelService( - configService, - new TestTextResourcePropertiesService(configService), - new TestThemeService(), - logService, - new UndoRedoService(dialogService, new TestNotificationService()), - disposables.add(new LanguageService()), - new TestLanguageConfigurationService(), - new LanguageFeatureDebounceService(logService), - new LanguageFeaturesService() - )); + const serviceCollection = new ServiceCollection([ + IConfigurationService, configService + ]); + + instantiationService = createModelServices(disposables, serviceCollection); + modelService = instantiationService.get(IModelService); }); teardown(() => { @@ -447,7 +444,7 @@ suite('ModelSemanticColoring', () => { test('DocumentSemanticTokens should be fetched when the result is empty if there are pending changes', async () => { await runWithFakedTimers({}, async () => { - disposables.add(ModesRegistry.registerLanguage({ id: 'testMode' })); + disposables.add(languageService.registerLanguage({ id: 'testMode' })); const inFirstCall = new Barrier(); const delayFirstResult = new Barrier(); @@ -502,7 +499,7 @@ suite('ModelSemanticColoring', () => { await runWithFakedTimers({}, async () => { let callCount = 0; - disposables.add(ModesRegistry.registerLanguage({ id: 'testMode2' })); + disposables.add(languageService.registerLanguage({ id: 'testMode2' })); disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode2', new class implements DocumentSemanticTokensProvider { getLegend(): SemanticTokensLegend { return { tokenTypes: ['class1'], tokenModifiers: [] }; diff --git a/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts b/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts index 030f9433805..b4cc8bfad6f 100644 --- a/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts +++ b/src/vs/editor/test/common/services/semanticTokensProviderStyling.test.ts @@ -7,19 +7,21 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens'; import { MetadataConsts } from 'vs/editor/common/languages'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling'; import { createModelServices } from 'vs/editor/test/common/testTextModel'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IThemeService, ITokenStyle } from 'vs/platform/theme/common/themeService'; +import { ILanguageService } from 'vs/editor/common/languages/language'; suite('ModelService', () => { let disposables: DisposableStore; let instantiationService: TestInstantiationService; + let languageService: ILanguageService; setup(() => { disposables = new DisposableStore(); instantiationService = createModelServices(disposables); + languageService = instantiationService.get(ILanguageService); }); teardown(() => { @@ -28,7 +30,7 @@ suite('ModelService', () => { test('issue #134973: invalid semantic tokens should be handled better', () => { const languageId = 'java'; - disposables.add(ModesRegistry.registerLanguage({ id: languageId })); + disposables.add(languageService.registerLanguage({ id: languageId })); const legend = { tokenTypes: ['st0', 'st1', 'st2', 'st3', 'st4', 'st5', 'st6', 'st7', 'st8', 'st9', 'st10'], tokenModifiers: [] diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 0c66ddc72a1..4ce9d88a0d8 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -973,7 +973,7 @@ declare namespace monaco.editor { * Create a new web worker that has model syncing capabilities built in. * Specify an AMD module to load that will `create` an object that will be proxied. */ - export function createWebWorker<T>(opts: IWebWorkerOptions): MonacoWebWorker<T>; + export function createWebWorker<T extends object>(opts: IWebWorkerOptions): MonacoWebWorker<T>; /** * Colorize the contents of `domNode` using attribute `data-lang`. @@ -2096,7 +2096,15 @@ declare namespace monaco.editor { /** * No preference. */ - None = 2 + None = 2, + /** + * If the given position is on injected text, prefers the position left of it. + */ + LeftOfInjectedText = 3, + /** + * If the given position is on injected text, prefers the position right of it. + */ + RightOfInjectedText = 4 } /** @@ -3724,6 +3732,12 @@ declare namespace monaco.editor { * Defaults to editor font family. */ fontFamily?: string; + /** + * The display style to render inlay hints with. + * Compact mode disables the borders and padding around the inlay hint. + * Defaults to 'standard'. + */ + displayStyle: 'standard' | 'compact'; } /** @@ -6680,6 +6694,11 @@ declare namespace monaco.languages { eol?: editor.EndOfLineSequence; }; + export interface SnippetTextEdit { + range: IRange; + snippet: string; + } + /** * Interface used to format a model */ diff --git a/src/vs/platform/configuration/test/common/testConfigurationService.ts b/src/vs/platform/configuration/test/common/testConfigurationService.ts index 23571bd5f6a..935454817ce 100644 --- a/src/vs/platform/configuration/test/common/testConfigurationService.ts +++ b/src/vs/platform/configuration/test/common/testConfigurationService.ts @@ -58,13 +58,19 @@ export class TestConfigurationService implements IConfigurationService { return Promise.resolve(undefined); } + private overrideIdentifiers: Map<string, string[]> = new Map(); + public setOverrideIdentifiers(key: string, identifiers: string[]): void { + this.overrideIdentifiers.set(key, identifiers); + } + public inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<T> { const config = this.getValue(undefined, overrides); return { value: getConfigurationValue<T>(config, key), defaultValue: getConfigurationValue<T>(config, key), - userValue: getConfigurationValue<T>(config, key) + userValue: getConfigurationValue<T>(config, key), + overrideIdentifiers: this.overrideIdentifiers.get(key) }; } diff --git a/src/vs/platform/driver/browser/baseDriver.ts b/src/vs/platform/driver/browser/baseDriver.ts deleted file mode 100644 index 9875672768a..00000000000 --- a/src/vs/platform/driver/browser/baseDriver.ts +++ /dev/null @@ -1,205 +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 { getClientArea, getTopLeftOffset } from 'vs/base/browser/dom'; -import { coalesce } from 'vs/base/common/arrays'; -import { language, locale } from 'vs/base/common/platform'; -import { IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver } from 'vs/platform/driver/common/driver'; -import localizedStrings from 'vs/platform/localizations/common/localizedStrings'; - -export abstract class BaseWindowDriver implements IWindowDriver { - - abstract click(selector: string, xoffset?: number, yoffset?: number): Promise<void>; - abstract doubleClick(selector: string): Promise<void>; - - async setValue(selector: string, text: string): Promise<void> { - const element = document.querySelector(selector); - - if (!element) { - return Promise.reject(new Error(`Element not found: ${selector}`)); - } - - const inputElement = element as HTMLInputElement; - inputElement.value = text; - - const event = new Event('input', { bubbles: true, cancelable: true }); - inputElement.dispatchEvent(event); - } - - async getTitle(): Promise<string> { - return document.title; - } - - async isActiveElement(selector: string): Promise<boolean> { - const element = document.querySelector(selector); - - if (element !== document.activeElement) { - const chain: string[] = []; - let el = document.activeElement; - - while (el) { - const tagName = el.tagName; - const id = el.id ? `#${el.id}` : ''; - const classes = coalesce(el.className.split(/\s+/g).map(c => c.trim())).map(c => `.${c}`).join(''); - chain.unshift(`${tagName}${id}${classes}`); - - el = el.parentElement; - } - - throw new Error(`Active element not found. Current active element is '${chain.join(' > ')}'. Looking for ${selector}`); - } - - return true; - } - - async getElements(selector: string, recursive: boolean): Promise<IElement[]> { - const query = document.querySelectorAll(selector); - const result: IElement[] = []; - for (let i = 0; i < query.length; i++) { - const element = query.item(i); - result.push(this.serializeElement(element, recursive)); - } - - return result; - } - - private serializeElement(element: Element, recursive: boolean): IElement { - const attributes = Object.create(null); - - for (let j = 0; j < element.attributes.length; j++) { - const attr = element.attributes.item(j); - if (attr) { - attributes[attr.name] = attr.value; - } - } - - const children: IElement[] = []; - - if (recursive) { - for (let i = 0; i < element.children.length; i++) { - const child = element.children.item(i); - if (child) { - children.push(this.serializeElement(child, true)); - } - } - } - - const { left, top } = getTopLeftOffset(element as HTMLElement); - - return { - tagName: element.tagName, - className: element.className, - textContent: element.textContent || '', - attributes, - children, - left, - top - }; - } - - async getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }> { - const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined; - return this._getElementXY(selector, offset); - } - - async typeInEditor(selector: string, text: string): Promise<void> { - const element = document.querySelector(selector); - - if (!element) { - throw new Error(`Editor not found: ${selector}`); - } - - const textarea = element as HTMLTextAreaElement; - const start = textarea.selectionStart; - const newStart = start + text.length; - const value = textarea.value; - const newValue = value.substr(0, start) + text + value.substr(start); - - textarea.value = newValue; - textarea.setSelectionRange(newStart, newStart); - - const event = new Event('input', { 'bubbles': true, 'cancelable': true }); - textarea.dispatchEvent(event); - } - - async getTerminalBuffer(selector: string): Promise<string[]> { - const element = document.querySelector(selector); - - if (!element) { - throw new Error(`Terminal not found: ${selector}`); - } - - const xterm = (element as any).xterm; - - if (!xterm) { - throw new Error(`Xterm not found: ${selector}`); - } - - const lines: string[] = []; - for (let i = 0; i < xterm.buffer.active.length; i++) { - lines.push(xterm.buffer.active.getLine(i)!.translateToString(true)); - } - - return lines; - } - - async writeInTerminal(selector: string, text: string): Promise<void> { - const element = document.querySelector(selector); - - if (!element) { - throw new Error(`Element not found: ${selector}`); - } - - const xterm = (element as any).xterm; - - if (!xterm) { - throw new Error(`Xterm not found: ${selector}`); - } - - xterm._core.coreService.triggerDataEvent(text); - } - - getLocaleInfo(): Promise<ILocaleInfo> { - return Promise.resolve({ - language: language, - locale: locale - }); - } - - getLocalizedStrings(): Promise<ILocalizedStrings> { - return Promise.resolve({ - open: localizedStrings.open, - close: localizedStrings.close, - find: localizedStrings.find - }); - } - - protected async _getElementXY(selector: string, offset?: { x: number; y: number }): Promise<{ x: number; y: number }> { - const element = document.querySelector(selector); - - if (!element) { - return Promise.reject(new Error(`Element not found: ${selector}`)); - } - - const { left, top } = getTopLeftOffset(element as HTMLElement); - const { width, height } = getClientArea(element as HTMLElement); - let x: number, y: number; - - if (offset) { - x = left + offset.x; - y = top + offset.y; - } else { - x = left + (width / 2); - y = top + (height / 2); - } - - x = Math.round(x); - y = Math.round(y); - - return { x, y }; - } - - abstract openDevTools(): Promise<void>; -} diff --git a/src/vs/platform/driver/browser/driver.ts b/src/vs/platform/driver/browser/driver.ts index 7027a0d37b6..c57855c3828 100644 --- a/src/vs/platform/driver/browser/driver.ts +++ b/src/vs/platform/driver/browser/driver.ts @@ -3,23 +3,210 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver'; +import { getClientArea, getTopLeftOffset } from 'vs/base/browser/dom'; +import { coalesce } from 'vs/base/common/arrays'; +import { language, locale } from 'vs/base/common/platform'; +import { IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver } from 'vs/platform/driver/common/driver'; +import localizedStrings from 'vs/platform/localizations/common/localizedStrings'; -class BrowserWindowDriver extends BaseWindowDriver { - click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise<void> { - throw new Error('Method not implemented.'); +export class BrowserWindowDriver implements IWindowDriver { + + async setValue(selector: string, text: string): Promise<void> { + const element = document.querySelector(selector); + + if (!element) { + return Promise.reject(new Error(`Element not found: ${selector}`)); + } + + const inputElement = element as HTMLInputElement; + inputElement.value = text; + + const event = new Event('input', { bubbles: true, cancelable: true }); + inputElement.dispatchEvent(event); } - doubleClick(selector: string): Promise<void> { - throw new Error('Method not implemented.'); + + async getTitle(): Promise<string> { + return document.title; + } + + async isActiveElement(selector: string): Promise<boolean> { + const element = document.querySelector(selector); + + if (element !== document.activeElement) { + const chain: string[] = []; + let el = document.activeElement; + + while (el) { + const tagName = el.tagName; + const id = el.id ? `#${el.id}` : ''; + const classes = coalesce(el.className.split(/\s+/g).map(c => c.trim())).map(c => `.${c}`).join(''); + chain.unshift(`${tagName}${id}${classes}`); + + el = el.parentElement; + } + + throw new Error(`Active element not found. Current active element is '${chain.join(' > ')}'. Looking for ${selector}`); + } + + return true; + } + + async getElements(selector: string, recursive: boolean): Promise<IElement[]> { + const query = document.querySelectorAll(selector); + const result: IElement[] = []; + for (let i = 0; i < query.length; i++) { + const element = query.item(i); + result.push(this.serializeElement(element, recursive)); + } + + return result; + } + + private serializeElement(element: Element, recursive: boolean): IElement { + const attributes = Object.create(null); + + for (let j = 0; j < element.attributes.length; j++) { + const attr = element.attributes.item(j); + if (attr) { + attributes[attr.name] = attr.value; + } + } + + const children: IElement[] = []; + + if (recursive) { + for (let i = 0; i < element.children.length; i++) { + const child = element.children.item(i); + if (child) { + children.push(this.serializeElement(child, true)); + } + } + } + + const { left, top } = getTopLeftOffset(element as HTMLElement); + + return { + tagName: element.tagName, + className: element.className, + textContent: element.textContent || '', + attributes, + children, + left, + top + }; + } + + async getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }> { + const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined; + return this._getElementXY(selector, offset); + } + + async typeInEditor(selector: string, text: string): Promise<void> { + const element = document.querySelector(selector); + + if (!element) { + throw new Error(`Editor not found: ${selector}`); + } + + const textarea = element as HTMLTextAreaElement; + const start = textarea.selectionStart; + const newStart = start + text.length; + const value = textarea.value; + const newValue = value.substr(0, start) + text + value.substr(start); + + textarea.value = newValue; + textarea.setSelectionRange(newStart, newStart); + + const event = new Event('input', { 'bubbles': true, 'cancelable': true }); + textarea.dispatchEvent(event); + } + + async getTerminalBuffer(selector: string): Promise<string[]> { + const element = document.querySelector(selector); + + if (!element) { + throw new Error(`Terminal not found: ${selector}`); + } + + const xterm = (element as any).xterm; + + if (!xterm) { + throw new Error(`Xterm not found: ${selector}`); + } + + const lines: string[] = []; + for (let i = 0; i < xterm.buffer.active.length; i++) { + lines.push(xterm.buffer.active.getLine(i)!.translateToString(true)); + } + + return lines; + } + + async writeInTerminal(selector: string, text: string): Promise<void> { + const element = document.querySelector(selector); + + if (!element) { + throw new Error(`Element not found: ${selector}`); + } + + const xterm = (element as any).xterm; + + if (!xterm) { + throw new Error(`Xterm not found: ${selector}`); + } + + xterm._core.coreService.triggerDataEvent(text); } - openDevTools(): Promise<void> { + + getLocaleInfo(): Promise<ILocaleInfo> { + return Promise.resolve({ + language: language, + locale: locale + }); + } + + getLocalizedStrings(): Promise<ILocalizedStrings> { + return Promise.resolve({ + open: localizedStrings.open, + close: localizedStrings.close, + find: localizedStrings.find + }); + } + + protected async _getElementXY(selector: string, offset?: { x: number; y: number }): Promise<{ x: number; y: number }> { + const element = document.querySelector(selector); + + if (!element) { + return Promise.reject(new Error(`Element not found: ${selector}`)); + } + + const { left, top } = getTopLeftOffset(element as HTMLElement); + const { width, height } = getClientArea(element as HTMLElement); + let x: number, y: number; + + if (offset) { + x = left + offset.x; + y = top + offset.y; + } else { + x = left + (width / 2); + y = top + (height / 2); + } + + x = Math.round(x); + y = Math.round(y); + + return { x, y }; + } + + click(selector: string, xoffset?: number, yoffset?: number): Promise<void> { + + // This is actually not used in the playwright drivers + // that can implement `click` natively via the driver + throw new Error('Method not implemented.'); } } -export async function registerWindowDriver(): Promise<IDisposable> { - (<any>window).driver = new BrowserWindowDriver(); - - return Disposable.None; +export function registerWindowDriver(): void { + Object.assign(window, { driver: new BrowserWindowDriver() }); } diff --git a/src/vs/platform/driver/common/driver.ts b/src/vs/platform/driver/common/driver.ts index ca36bc4409b..6d23ce9748c 100644 --- a/src/vs/platform/driver/common/driver.ts +++ b/src/vs/platform/driver/common/driver.ts @@ -44,10 +44,9 @@ export interface IDriver { startTracing(windowId: number, name: string): Promise<void>; stopTracing(windowId: number, name: string, persist: boolean): Promise<void>; reloadWindow(windowId: number): Promise<void>; - exitApplication(): Promise<boolean>; + exitApplication(): Promise<number /* main PID */>; dispatchKeybinding(windowId: number, keybinding: string): Promise<void>; click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise<void>; - doubleClick(windowId: number, selector: string): Promise<void>; setValue(windowId: number, selector: string, text: string): Promise<void>; getTitle(windowId: number): Promise<string>; isActiveElement(windowId: number, selector: string): Promise<boolean>; @@ -62,7 +61,6 @@ export interface IDriver { export interface IWindowDriver { click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise<void>; - doubleClick(selector: string): Promise<void>; setValue(selector: string, text: string): Promise<void>; getTitle(): Promise<string>; isActiveElement(selector: string): Promise<boolean>; @@ -79,11 +77,7 @@ export interface IWindowDriver { export const ID = 'driverService'; export const IDriver = createDecorator<IDriver>(ID); -export interface IDriverOptions { - verbose: boolean; -} - export interface IWindowDriverRegistry { - registerWindowDriver(windowId: number): Promise<IDriverOptions>; + registerWindowDriver(windowId: number): Promise<void>; reloadWindowDriver(windowId: number): Promise<void>; } diff --git a/src/vs/platform/driver/common/driverIpc.ts b/src/vs/platform/driver/common/driverIpc.ts index 49593c75882..6d5ed5e55c4 100644 --- a/src/vs/platform/driver/common/driverIpc.ts +++ b/src/vs/platform/driver/common/driverIpc.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IDriverOptions, IElement, ILocaleInfo, ILocalizedStrings as ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; +import { IElement, ILocaleInfo, ILocalizedStrings as ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; export class WindowDriverChannel implements IServerChannel { @@ -18,7 +18,6 @@ export class WindowDriverChannel implements IServerChannel { call(_: unknown, command: string, arg?: any): Promise<any> { switch (command) { case 'click': return this.driver.click(arg[0], arg[1], arg[2]); - case 'doubleClick': return this.driver.doubleClick(arg); case 'setValue': return this.driver.setValue(arg[0], arg[1]); case 'getTitle': return this.driver.getTitle(); case 'isActiveElement': return this.driver.isActiveElement(arg); @@ -45,10 +44,6 @@ export class WindowDriverChannelClient implements IWindowDriver { return this.channel.call('click', [selector, xoffset, yoffset]); } - doubleClick(selector: string): Promise<void> { - return this.channel.call('doubleClick', selector); - } - setValue(selector: string, text: string): Promise<void> { return this.channel.call('setValue', [selector, text]); } @@ -96,7 +91,7 @@ export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry constructor(private channel: IChannel) { } - registerWindowDriver(windowId: number): Promise<IDriverOptions> { + registerWindowDriver(windowId: number): Promise<void> { return this.channel.call('registerWindowDriver', windowId); } diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts index aa1e5f6ee79..f14f14e40ad 100644 --- a/src/vs/platform/driver/electron-main/driver.ts +++ b/src/vs/platform/driver/electron-main/driver.ts @@ -12,7 +12,7 @@ import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; import { OS } from 'vs/base/common/platform'; import { IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net'; -import { IDriver, IDriverOptions, IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; +import { IDriver, IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; import { WindowDriverChannelClient } from 'vs/platform/driver/common/driverIpc'; import { DriverChannel, WindowDriverRegistryChannel } from 'vs/platform/driver/node/driver'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; @@ -40,7 +40,6 @@ export class Driver implements IDriver, IWindowDriverRegistry { constructor( private windowServer: IPCServer, - private options: IDriverOptions, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IFileService private readonly fileService: IFileService, @@ -48,13 +47,12 @@ export class Driver implements IDriver, IWindowDriverRegistry { @ILogService private readonly logService: ILogService ) { } - async registerWindowDriver(windowId: number): Promise<IDriverOptions> { + async registerWindowDriver(windowId: number): Promise<void> { this.logService.info(`[driver] registerWindowDriver(${windowId})`); this.registeredWindowIds.add(windowId); this.reloadingWindowIds.delete(windowId); this.onDidReloadingChange.fire(); - return this.options; } async reloadWindowDriver(windowId: number): Promise<void> { @@ -111,10 +109,12 @@ export class Driver implements IDriver, IWindowDriverRegistry { this.lifecycleMainService.reload(window); } - exitApplication(): Promise<boolean> { + async exitApplication(): Promise<number> { this.logService.info(`[driver] exitApplication()`); - return this.lifecycleMainService.quit(); + this.lifecycleMainService.quit(); + + return process.pid; } async dispatchKeybinding(windowId: number, keybinding: string): Promise<void> { @@ -175,11 +175,6 @@ export class Driver implements IDriver, IWindowDriverRegistry { await windowDriver.click(selector, xoffset, yoffset); } - async doubleClick(windowId: number, selector: string): Promise<void> { - const windowDriver = await this.getWindowDriver(windowId); - await windowDriver.doubleClick(selector); - } - async setValue(windowId: number, selector: string, text: string): Promise<void> { const windowDriver = await this.getWindowDriver(windowId); await windowDriver.setValue(selector, text); @@ -249,11 +244,9 @@ export class Driver implements IDriver, IWindowDriverRegistry { export async function serve( windowServer: IPCServer, handle: string, - environmentMainService: IEnvironmentMainService, instantiationService: IInstantiationService ): Promise<IDisposable> { - const verbose = environmentMainService.driverVerbose; - const driver = instantiationService.createInstance(Driver, windowServer, { verbose }); + const driver = instantiationService.createInstance(Driver, windowServer); const windowDriverRegistryChannel = new WindowDriverRegistryChannel(driver); windowServer.registerChannel('windowDriverRegistry', windowDriverRegistryChannel); diff --git a/src/vs/platform/driver/electron-sandbox/driver.ts b/src/vs/platform/driver/electron-sandbox/driver.ts index 84bc965bd9b..bc2e4c9ae38 100644 --- a/src/vs/platform/driver/electron-sandbox/driver.ts +++ b/src/vs/platform/driver/electron-sandbox/driver.ts @@ -5,13 +5,32 @@ import { timeout } from 'vs/base/common/async'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver'; +import { BrowserWindowDriver } from 'vs/platform/driver/browser/driver'; import { WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/common/driverIpc'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; -class WindowDriver extends BaseWindowDriver { +interface INativeWindowDriverHelper { + exitApplication(): Promise<number /* main process PID */>; +} + +class NativeWindowDriver extends BrowserWindowDriver { + + constructor(private readonly helper: INativeWindowDriverHelper) { + super(); + } + + exitApplication(): Promise<number> { + return this.helper.exitApplication(); + } +} + +export function registerWindowDriver(helper: INativeWindowDriverHelper): void { + Object.assign(window, { driver: new NativeWindowDriver(helper) }); +} + +class LegacyNativeWindowDriver extends BrowserWindowDriver { constructor( @INativeHostService private readonly nativeHostService: INativeHostService @@ -19,16 +38,13 @@ class WindowDriver extends BaseWindowDriver { super(); } - click(selector: string, xoffset?: number, yoffset?: number): Promise<void> { + override click(selector: string, xoffset?: number, yoffset?: number): Promise<void> { const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined; - return this._click(selector, 1, offset); - } - doubleClick(selector: string): Promise<void> { - return this._click(selector, 2); + return this.doClick(selector, 1, offset); } - private async _click(selector: string, clickCount: number, offset?: { x: number; y: number }): Promise<void> { + private async doClick(selector: string, clickCount: number, offset?: { x: number; y: number }): Promise<void> { const { x, y } = await this._getElementXY(selector, offset); await this.nativeHostService.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any); @@ -37,17 +53,19 @@ class WindowDriver extends BaseWindowDriver { await this.nativeHostService.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any); await timeout(100); } - - async openDevTools(): Promise<void> { - await this.nativeHostService.openDevTools({ mode: 'detach' }); - } } -export async function registerWindowDriver(accessor: ServicesAccessor, windowId: number): Promise<IDisposable> { +/** + * Old school window driver that is implemented by us + * from the main process. + * + * @deprecated + */ +export async function registerLegacyWindowDriver(accessor: ServicesAccessor, windowId: number): Promise<IDisposable> { const instantiationService = accessor.get(IInstantiationService); const mainProcessService = accessor.get(IMainProcessService); - const windowDriver = instantiationService.createInstance(WindowDriver); + const windowDriver = instantiationService.createInstance(LegacyNativeWindowDriver); const windowDriverChannel = new WindowDriverChannel(windowDriver); mainProcessService.registerChannel('windowDriver', windowDriverChannel); diff --git a/src/vs/platform/driver/node/driver.ts b/src/vs/platform/driver/node/driver.ts index 4e56bc540e4..29abf734bec 100644 --- a/src/vs/platform/driver/node/driver.ts +++ b/src/vs/platform/driver/node/driver.ts @@ -27,7 +27,6 @@ export class DriverChannel implements IServerChannel { case 'exitApplication': return this.driver.exitApplication(); case 'dispatchKeybinding': return this.driver.dispatchKeybinding(arg[0], arg[1]); case 'click': return this.driver.click(arg[0], arg[1], arg[2], arg[3]); - case 'doubleClick': return this.driver.doubleClick(arg[0], arg[1]); case 'setValue': return this.driver.setValue(arg[0], arg[1], arg[2]); case 'getTitle': return this.driver.getTitle(arg[0]); case 'isActiveElement': return this.driver.isActiveElement(arg[0], arg[1]); @@ -70,7 +69,7 @@ export class DriverChannelClient implements IDriver { return this.channel.call('reloadWindow', windowId); } - exitApplication(): Promise<boolean> { + exitApplication(): Promise<number> { return this.channel.call('exitApplication'); } @@ -82,10 +81,6 @@ export class DriverChannelClient implements IDriver { return this.channel.call('click', [windowId, selector, xoffset, yoffset]); } - doubleClick(windowId: number, selector: string): Promise<void> { - return this.channel.call('doubleClick', [windowId, selector]); - } - setValue(windowId: number, selector: string, text: string): Promise<void> { return this.channel.call('setValue', [windowId, selector, text]); } diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 4815b4a2888..ccac10545b5 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -79,8 +79,11 @@ export interface NativeParsedArgs { 'max-memory'?: string; 'file-write'?: boolean; 'file-chmod'?: boolean; + /** + * @deprecated use `enable-smoke-test-driver` + */ 'driver'?: string; - 'driver-verbose'?: boolean; + 'enable-smoke-test-driver'?: boolean; 'remote'?: string; 'force'?: boolean; 'do-not-sync'?: boolean; diff --git a/src/vs/platform/environment/electron-main/environmentMainService.ts b/src/vs/platform/environment/electron-main/environmentMainService.ts index beb1896fa2b..6834b8115fe 100644 --- a/src/vs/platform/environment/electron-main/environmentMainService.ts +++ b/src/vs/platform/environment/electron-main/environmentMainService.ts @@ -35,7 +35,6 @@ export interface IEnvironmentMainService extends INativeEnvironmentService { // --- config sandbox: boolean; - driverVerbose: boolean; disableUpdates: boolean; } @@ -60,9 +59,6 @@ export class EnvironmentMainService extends NativeEnvironmentService implements get sandbox(): boolean { return !!this.args['__sandbox']; } @memoize - get driverVerbose(): boolean { return !!this.args['driver-verbose']; } - - @memoize get disableUpdates(): boolean { return !!this.args['disable-updates']; } @memoize diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index b20c5c57c2d..1d7aadc1f14 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -99,6 +99,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = { 'export-default-configuration': { type: 'string' }, 'install-source': { type: 'string' }, 'driver': { type: 'string' }, + 'enable-smoke-test-driver': { type: 'boolean' }, 'logExtensionHostCommunication': { type: 'boolean' }, 'skip-release-notes': { type: 'boolean' }, 'skip-welcome': { type: 'boolean' }, @@ -114,7 +115,6 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = { 'open-url': { type: 'boolean' }, 'file-write': { type: 'boolean' }, 'file-chmod': { type: 'boolean' }, - 'driver-verbose': { type: 'boolean' }, 'install-builtin-extension': { type: 'string[]' }, 'force': { type: 'boolean' }, 'do-not-sync': { type: 'boolean' }, @@ -212,7 +212,7 @@ export function parseArgs<T>(args: string[], options: OptionDescriptions<T>, err if (!val) { val = remainingArgs[deprecatedId]; if (val) { - errorReporter.onDeprecatedOption(deprecatedId, o.deprecationMessage || localize('deprecated.useInstead', 'Use {0} instead.')); + errorReporter.onDeprecatedOption(deprecatedId, o.deprecationMessage || localize('deprecated.useInstead', 'Use {0} instead.', optionId)); } } delete remainingArgs[deprecatedId]; diff --git a/src/vs/platform/extensionManagement/common/extensionStorage.ts b/src/vs/platform/extensionManagement/common/extensionStorage.ts index 4cd7ad6603a..13ddc8bd4f5 100644 --- a/src/vs/platform/extensionManagement/common/extensionStorage.ts +++ b/src/vs/platform/extensionManagement/common/extensionStorage.ts @@ -7,14 +7,14 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { adoptToGalleryExtensionId, getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { adoptToGalleryExtensionId, areSameExtensions, getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IProductService } from 'vs/platform/product/common/productService'; import { distinct } from 'vs/base/common/arrays'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtension } from 'vs/platform/extensions/common/extensions'; import { isArray, isString } from 'vs/base/common/types'; import { IStringDictionary } from 'vs/base/common/collections'; -import { IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; export interface IExtensionIdWithVersion { id: string; @@ -55,10 +55,42 @@ export class ExtensionStorageService extends Disposable implements IExtensionSto return undefined; } + static async removeOutdatedExtensionVersions(extensionManagementService: IExtensionManagementService, storageService: IStorageService): Promise<void> { + const extensions = await extensionManagementService.getInstalled(); + const extensionVersionsToRemove: string[] = []; + for (const [id, versions] of ExtensionStorageService.readAllExtensionsWithKeysForSync(storageService)) { + const extensionVersion = extensions.find(e => areSameExtensions(e.identifier, { id }))?.manifest.version; + for (const version of versions) { + if (extensionVersion !== version) { + extensionVersionsToRemove.push(ExtensionStorageService.toKey({ id, version })); + } + } + } + for (const key of extensionVersionsToRemove) { + storageService.remove(key, StorageScope.GLOBAL); + } + } + + private static readAllExtensionsWithKeysForSync(storageService: IStorageService): Map<string, string[]> { + const extensionsWithKeysForSync = new Map<string, string[]>(); + const keys = storageService.keys(StorageScope.GLOBAL, StorageTarget.MACHINE); + for (const key of keys) { + const extensionIdWithVersion = ExtensionStorageService.fromKey(key); + if (extensionIdWithVersion) { + let versions = extensionsWithKeysForSync.get(extensionIdWithVersion.id.toLowerCase()); + if (!versions) { + extensionsWithKeysForSync.set(extensionIdWithVersion.id.toLowerCase(), versions = []); + } + versions.push(extensionIdWithVersion.version); + } + } + return extensionsWithKeysForSync; + } + private readonly _onDidChangeExtensionStorageToSync = this._register(new Emitter<void>()); readonly onDidChangeExtensionStorageToSync = this._onDidChangeExtensionStorageToSync.event; - private readonly extensionsWithKeysForSync = new Set<string>(); + private readonly extensionsWithKeysForSync: Map<string, string[]>; constructor( @IStorageService private readonly storageService: IStorageService, @@ -66,20 +98,10 @@ export class ExtensionStorageService extends Disposable implements IExtensionSto @ILogService private readonly logService: ILogService, ) { super(); - this.initialize(); + this.extensionsWithKeysForSync = ExtensionStorageService.readAllExtensionsWithKeysForSync(storageService); this._register(this.storageService.onDidChangeValue(e => this.onDidChangeStorageValue(e))); } - private initialize(): void { - const keys = this.storageService.keys(StorageScope.GLOBAL, StorageTarget.MACHINE); - for (const key of keys) { - const extensionIdWithVersion = ExtensionStorageService.fromKey(key); - if (extensionIdWithVersion) { - this.extensionsWithKeysForSync.add(extensionIdWithVersion.id.toLowerCase()); - } - } - } - private onDidChangeStorageValue(e: IStorageValueChangeEvent): void { if (e.scope !== StorageScope.GLOBAL) { return; @@ -94,8 +116,16 @@ export class ExtensionStorageService extends Disposable implements IExtensionSto // Keys for sync of an extension has changed const extensionIdWithVersion = ExtensionStorageService.fromKey(e.key); if (extensionIdWithVersion) { - this.extensionsWithKeysForSync.add(extensionIdWithVersion.id.toLowerCase()); - this._onDidChangeExtensionStorageToSync.fire(); + if (this.storageService.get(e.key, StorageScope.GLOBAL) === undefined) { + this.extensionsWithKeysForSync.delete(extensionIdWithVersion.id.toLowerCase()); + } else { + let versions = this.extensionsWithKeysForSync.get(extensionIdWithVersion.id.toLowerCase()); + if (!versions) { + this.extensionsWithKeysForSync.set(extensionIdWithVersion.id.toLowerCase(), versions = []); + } + versions.push(extensionIdWithVersion.version); + this._onDidChangeExtensionStorageToSync.fire(); + } return; } } @@ -180,4 +210,5 @@ export class ExtensionStorageService extends Disposable implements IExtensionSto this.storageService.remove('extensionStorage.migrationList', StorageScope.GLOBAL); } } + } diff --git a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts index 36c669a4951..5eda8e0d4b2 100644 --- a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts +++ b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts @@ -45,6 +45,11 @@ export class ExtensionsWatcher extends Disposable { } private doesChangeAffects(change: IFileChange, extensionsResource: URI): boolean { + // Only interested in added/deleted changes + if (change.type !== FileChangeType.ADDED && change.type !== FileChangeType.DELETED) { + return false; + } + // Is not immediate child of extensions resource if (!this.uriIdentityService.extUri.isEqual(this.uriIdentityService.extUri.dirname(change.resource), extensionsResource)) { return false; @@ -55,11 +60,6 @@ export class ExtensionsWatcher extends Disposable { return true; } - // Only interested in added/deleted changes - if (change.type !== FileChangeType.ADDED && change.type !== FileChangeType.DELETED) { - return false; - } - // Ingore changes to files starting with `.` if (this.uriIdentityService.extUri.basename(change.resource).startsWith('.')) { return false; diff --git a/src/vs/platform/files/common/diskFileSystemProvider.ts b/src/vs/platform/files/common/diskFileSystemProvider.ts index d65e3a20028..431a28d563d 100644 --- a/src/vs/platform/files/common/diskFileSystemProvider.ts +++ b/src/vs/platform/files/common/diskFileSystemProvider.ts @@ -69,7 +69,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable { private watchUniversal(resource: URI, opts: IWatchOptions): IDisposable { // Add to list of paths to watch universally - const pathToWatch: IUniversalWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes, recursive: opts.recursive }; + const pathToWatch: IUniversalWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes, includes: opts.includes, recursive: opts.recursive }; const remove = insert(this.universalPathsToWatch, pathToWatch); // Trigger update @@ -150,7 +150,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable { private watchNonRecursive(resource: URI, opts: IWatchOptions): IDisposable { // Add to list of paths to watch non-recursively - const pathToWatch: INonRecursiveWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes, recursive: false }; + const pathToWatch: INonRecursiveWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes, includes: opts.includes, recursive: false }; const remove = insert(this.nonRecursivePathsToWatch, pathToWatch); // Trigger update diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index d962c778da6..1b75291e899 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -428,9 +428,16 @@ export interface IWatchOptions { readonly recursive: boolean; /** - * A set of paths to exclude from watching. + * A set of glob patterns or paths to exclude from watching. */ excludes: string[]; + + /** + * An optional set of glob patterns or paths to include for + * watching. If not provided, all paths are considered for + * events. + */ + includes?: string[]; } export const enum FileSystemProviderCapabilities { diff --git a/src/vs/platform/files/common/watcher.ts b/src/vs/platform/files/common/watcher.ts index d4dd3e52802..78614c324dc 100644 --- a/src/vs/platform/files/common/watcher.ts +++ b/src/vs/platform/files/common/watcher.ts @@ -25,6 +25,13 @@ interface IWatchRequest { * A set of glob patterns or paths to exclude from watching. */ excludes: string[]; + + /** + * An optional set of glob patterns or paths to include for + * watching. If not provided, all paths are considered for + * events. + */ + includes?: string[]; } export interface INonRecursiveWatchRequest extends IWatchRequest { diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts index 15ae4d7b224..df6bd3f016c 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts @@ -49,19 +49,19 @@ export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher { return true; // not yet watching that path } - // Re-watch path if excludes have changed - return !equals(watcher.request.excludes, request.excludes); + // Re-watch path if excludes or includes have changed + return !equals(watcher.request.excludes, request.excludes) || !equals(watcher.request.includes, request.includes); }); // Gather paths that we should stop watching const pathsToStopWatching = Array.from(this.watchers.values()).filter(({ request }) => { - return !normalizedRequests.find(normalizedRequest => normalizedRequest.path === request.path && equals(normalizedRequest.excludes, request.excludes)); + return !normalizedRequests.find(normalizedRequest => normalizedRequest.path === request.path && equals(normalizedRequest.excludes, request.excludes) && equals(normalizedRequest.includes, request.includes)); }).map(({ request }) => request.path); // Logging if (requestsToStartWatching.length) { - this.trace(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : '<none>'})`).join(',')}`); + this.trace(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : '<none>'}, includes: ${request.includes && request.includes.length > 0 ? request.includes : '<all>'})`).join(',')}`); } if (pathsToStopWatching.length) { diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index 2bf8f9ecea7..c23ea589dbb 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts @@ -48,6 +48,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { private fileChangesBuffer: IDiskFileChange[] = []; private readonly excludes = this.request.excludes.map(exclude => parse(exclude)); + private readonly includes = this.request.includes?.map(include => parse(include)); private readonly cts = new CancellationTokenSource(); @@ -317,14 +318,14 @@ export class NodeJSFileWatcherLibrary extends Disposable { // File still exists, so emit as change event and reapply the watcher if (fileExists) { - this.onFileChange({ path: this.request.path, type: FileChangeType.UPDATED }, true /* skip excludes (file is explicitly watched) */); + this.onFileChange({ path: this.request.path, type: FileChangeType.UPDATED }, true /* skip excludes/includes (file is explicitly watched) */); disposables.add(await this.doWatch(path, false)); } // File seems to be really gone, so emit a deleted event and dispose else { - const eventPromise = this.onFileChange({ path: this.request.path, type: FileChangeType.DELETED }, true /* skip excludes (file is explicitly watched) */); + const eventPromise = this.onFileChange({ path: this.request.path, type: FileChangeType.DELETED }, true /* skip excludes/includes (file is explicitly watched) */); // Important to await the event delivery // before disposing the watcher, otherwise @@ -342,7 +343,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { // File changed else { - this.onFileChange({ path: this.request.path, type: FileChangeType.UPDATED }, true /* skip excludes (file is explicitly watched) */); + this.onFileChange({ path: this.request.path, type: FileChangeType.UPDATED }, true /* skip excludes/includes (file is explicitly watched) */); } } }); @@ -358,7 +359,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { }); } - private async onFileChange(event: IDiskFileChange, skipExcludes = false): Promise<void> { + private async onFileChange(event: IDiskFileChange, skipIncludeExcludeChecks = false): Promise<void> { if (this.cts.token.isCancellationRequested) { return; } @@ -368,10 +369,14 @@ export class NodeJSFileWatcherLibrary extends Disposable { this.trace(`${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`); } - // Add to buffer unless ignored (not if explicitly disabled) - if (!skipExcludes && this.excludes.some(exclude => exclude(event.path))) { + // Add to buffer unless excluded or not included (not if explicitly disabled) + if (!skipIncludeExcludeChecks && this.excludes.some(exclude => exclude(event.path))) { if (this.verboseLogging) { - this.trace(` >> ignored ${event.path}`); + this.trace(` >> ignored (excluded) ${event.path}`); + } + } else if (!skipIncludeExcludeChecks && this.includes && this.includes.length > 0 && !this.includes.some(include => include(event.path))) { + if (this.verboseLogging) { + this.trace(` >> ignored (not included) ${event.path}`); } } else { this.fileChangesBuffer.push(event); diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index 4fdec47ca5f..85388db8f80 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -128,8 +128,8 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { return true; // not yet watching that path } - // Re-watch path if excludes have changed or polling interval - return !equals(watcher.request.excludes, request.excludes) || watcher.request.pollingInterval !== request.pollingInterval; + // Re-watch path if excludes/includes have changed or polling interval + return !equals(watcher.request.excludes, request.excludes) || !equals(watcher.request.includes, request.includes) || watcher.request.pollingInterval !== request.pollingInterval; }); // Gather paths that we should stop watching @@ -137,6 +137,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { return !normalizedRequests.find(normalizedRequest => { return normalizedRequest.path === request.path && equals(normalizedRequest.excludes, request.excludes) && + equals(normalizedRequest.includes, request.includes) && normalizedRequest.pollingInterval === request.pollingInterval; }); @@ -145,7 +146,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { // Logging if (requestsToStartWatching.length) { - this.trace(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : '<none>'})`).join(',')}`); + this.trace(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : '<none>'}, includes: ${request.includes && request.includes.length > 0 ? request.includes : '<all>'})`).join(',')}`); } if (pathsToStopWatching.length) { @@ -283,8 +284,9 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { // Path checks for symbolic links / wrong casing const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); - // Warm up exclude patterns for usage + // Warm up exclude/include patterns for usage const excludePatterns = request.excludes.map(exclude => parse(exclude)); + const includePatterns = request.includes?.map(include => parse(include)); const ignore = this.toExcludePaths(realPath, watcher.request.excludes); @@ -308,7 +310,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { } // Handle & emit events - this.onParcelEvents(parcelEvents, watcher, excludePatterns, realPathDiffers, realPathLength); + this.onParcelEvents(parcelEvents, watcher, excludePatterns, includePatterns, realPathDiffers, realPathLength); } // Store a snapshot of files to the snapshot file @@ -352,8 +354,9 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { // Path checks for symbolic links / wrong casing const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); - // Warm up exclude patterns for usage + // Warm up exclude/include patterns for usage const excludePatterns = request.excludes.map(exclude => parse(exclude)); + const includePatterns = request.includes?.map(include => parse(include)); const ignore = this.toExcludePaths(realPath, watcher.request.excludes); parcelWatcher.subscribe(realPath, (error, parcelEvents) => { @@ -370,7 +373,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { } // Handle & emit events - this.onParcelEvents(parcelEvents, watcher, excludePatterns, realPathDiffers, realPathLength); + this.onParcelEvents(parcelEvents, watcher, excludePatterns, includePatterns, realPathDiffers, realPathLength); }, { backend: ParcelWatcher.PARCEL_WATCHER_BACKEND, ignore @@ -385,13 +388,13 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { }); } - private onParcelEvents(parcelEvents: parcelWatcher.Event[], watcher: IParcelWatcherInstance, excludes: ParsedPattern[], realPathDiffers: boolean, realPathLength: number): void { + private onParcelEvents(parcelEvents: parcelWatcher.Event[], watcher: IParcelWatcherInstance, excludes: ParsedPattern[], includes: ParsedPattern[] | undefined, realPathDiffers: boolean, realPathLength: number): void { if (parcelEvents.length === 0) { return; } // Check for excludes - const rawEvents = this.handleExcludes(parcelEvents, excludes); + const rawEvents = this.handleExcludeIncludes(parcelEvents, excludes, includes); // Normalize events: handle NFC normalization and symlinks const { events: normalizedEvents, rootDeleted } = this.normalizeEvents(rawEvents, watcher.request, realPathDiffers, realPathLength); @@ -411,7 +414,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { } } - private handleExcludes(parcelEvents: parcelWatcher.Event[], excludes: ParsedPattern[]): IDiskFileChange[] { + private handleExcludeIncludes(parcelEvents: parcelWatcher.Event[], excludes: ParsedPattern[], includes: ParsedPattern[] | undefined): IDiskFileChange[] { const events: IDiskFileChange[] = []; for (const { path, type: parcelEventType } of parcelEvents) { @@ -420,9 +423,14 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { this.trace(`${type === FileChangeType.ADDED ? '[ADDED]' : type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`); } + // Add to buffer unless excluded or not included (not if explicitly disabled) if (excludes.some(exclude => exclude(path))) { if (this.verboseLogging) { - this.trace(` >> ignored ${path}`); + this.trace(` >> ignored (excluded) ${path}`); + } + } else if (includes && includes.length > 0 && !includes.some(include => include(path))) { + if (this.verboseLogging) { + this.trace(` >> ignored (not included) ${path}`); } } else { events.push({ type, path }); diff --git a/src/vs/platform/files/test/browser/fileService.test.ts b/src/vs/platform/files/test/browser/fileService.test.ts index 5fda9fd3f54..79b44bdb46a 100644 --- a/src/vs/platform/files/test/browser/fileService.test.ts +++ b/src/vs/platform/files/test/browser/fileService.test.ts @@ -121,6 +121,7 @@ suite('File Service', () => { const resource3 = URI.parse('test://foo/bar3'); const watcher3Disposable1 = service.watch(resource3); const watcher3Disposable2 = service.watch(resource3, { recursive: true, excludes: [] }); + const watcher3Disposable3 = service.watch(resource3, { recursive: false, excludes: [], includes: [] }); await timeout(0); // service.watch() is async assert.strictEqual(disposeCounter, 0); @@ -128,6 +129,8 @@ suite('File Service', () => { assert.strictEqual(disposeCounter, 1); watcher3Disposable2.dispose(); assert.strictEqual(disposeCounter, 2); + watcher3Disposable3.dispose(); + assert.strictEqual(disposeCounter, 3); service.dispose(); }); diff --git a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts index f45d208f814..d617ce2b2e4 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts @@ -372,6 +372,26 @@ import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatch return basicCrudTest(filePath, true); }); + test('includes can be updated (folder watch)', async function () { + await watcher.watch([{ path: testDir, excludes: [], includes: ['nothing'], recursive: false }]); + await watcher.watch([{ path: testDir, excludes: [], recursive: false }]); + + return basicCrudTest(join(testDir, 'files-includes.txt')); + }); + + test('non-includes are ignored (file watch)', async function () { + const filePath = join(testDir, 'lorem.txt'); + await watcher.watch([{ path: filePath, excludes: [], includes: ['nothing'], recursive: false }]); + + return basicCrudTest(filePath, true); + }); + + test('includes are supported (folder watch)', async function () { + await watcher.watch([{ path: testDir, excludes: [], includes: ['**/files-includes.txt'], recursive: false }]); + + return basicCrudTest(join(testDir, 'files-includes.txt')); + }); + (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (folder watch)', async function () { const link = join(testDir, 'deep-linked'); const linkTarget = join(testDir, 'deep'); @@ -495,5 +515,3 @@ import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatch return watchPromise; }); }); - -// TODO test for excludes? subsequent updates to rewatch like parcel? diff --git a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index 722b31aa579..5529a525e0c 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts @@ -155,7 +155,7 @@ import { ltrim } from 'vs/base/common/strings'; } test('basics', async function () { - await watcher.watch([{ path: testDir, excludes: [], recursive: true }]); + await watcher.watch([{ path: testDir, excludes: [], recursive: true }]); // // New file const newFilePath = join(testDir, 'deep', 'newFile.txt'); @@ -437,6 +437,19 @@ import { ltrim } from 'vs/base/common/strings'; return basicCrudTest(join(testDir, 'deep', 'newFile.txt')); }); + test('subsequent watch updates watchers (includes)', async function () { + await watcher.watch([{ path: testDir, excludes: [], includes: ['nothing'], recursive: true }]); + await watcher.watch([{ path: testDir, excludes: [], recursive: true }]); + + return basicCrudTest(join(testDir, 'deep', 'newFile.txt')); + }); + + test('includes are supported', async function () { + await watcher.watch([{ path: testDir, excludes: [], includes: ['**/deep/**'], recursive: true }]); + + return basicCrudTest(join(testDir, 'deep', 'newFile.txt')); + }); + (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (root)', async function () { const link = join(testDir, 'deep-linked'); const linkTarget = join(testDir, 'deep'); diff --git a/src/vs/platform/ipc/electron-sandbox/services.ts b/src/vs/platform/ipc/electron-sandbox/services.ts index 8c78c98f03b..b2e63aaebf4 100644 --- a/src/vs/platform/ipc/electron-sandbox/services.ts +++ b/src/vs/platform/ipc/electron-sandbox/services.ts @@ -11,7 +11,7 @@ import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/co type ChannelClientCtor<T> = { new(channel: IChannel): T }; type Remote = { getChannel(channelName: string): IChannel }; -abstract class RemoteServiceStub<T> { +abstract class RemoteServiceStub<T extends object> { constructor( channelName: string, options: IRemoteServiceWithChannelClientOptions<T> | IRemoteServiceWithProxyOptions | undefined, @@ -55,7 +55,7 @@ export interface IMainProcessService { registerChannel(channelName: string, channel: IServerChannel<string>): void; } -class MainProcessRemoteServiceStub<T> extends RemoteServiceStub<T> { +class MainProcessRemoteServiceStub<T extends object> extends RemoteServiceStub<T> { constructor(channelName: string, options: IRemoteServiceWithChannelClientOptions<T> | IRemoteServiceWithProxyOptions | undefined, @IMainProcessService ipcService: IMainProcessService) { super(channelName, options, ipcService); } @@ -81,7 +81,7 @@ export interface ISharedProcessService { notifyRestored(): void; } -class SharedProcessRemoteServiceStub<T> extends RemoteServiceStub<T> { +class SharedProcessRemoteServiceStub<T extends object> extends RemoteServiceStub<T> { constructor(channelName: string, options: IRemoteServiceWithChannelClientOptions<T> | IRemoteServiceWithProxyOptions | undefined, @ISharedProcessService ipcService: ISharedProcessService) { super(channelName, options, ipcService); } diff --git a/src/vs/platform/state/electron-main/stateMainService.ts b/src/vs/platform/state/electron-main/stateMainService.ts index cb1f4be3e02..3c9e0d74eb3 100644 --- a/src/vs/platform/state/electron-main/stateMainService.ts +++ b/src/vs/platform/state/electron-main/stateMainService.ts @@ -154,7 +154,7 @@ export class StateMainService implements IStateMainService { private static readonly STATE_FILE = 'storage.json'; private readonly legacyStateFilePath = URI.file(join(this.environmentMainService.userDataPath, StateMainService.STATE_FILE)); - private readonly stateFilePath = joinPath(this.environmentMainService.appSettingsHome, StateMainService.STATE_FILE); + private readonly stateFilePath = joinPath(this.environmentMainService.globalStorageHome, StateMainService.STATE_FILE); private readonly fileStorage: FileStorage; diff --git a/src/vs/platform/terminal/common/terminalProfiles.ts b/src/vs/platform/terminal/common/terminalProfiles.ts index 1b4d411e501..37b166d82c1 100644 --- a/src/vs/platform/terminal/common/terminalProfiles.ts +++ b/src/vs/platform/terminal/common/terminalProfiles.ts @@ -5,19 +5,24 @@ import { Codicon } from 'vs/base/common/codicons'; import { URI, UriComponents } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; import { IExtensionTerminalProfile, ITerminalProfile, TerminalIcon } from 'vs/platform/terminal/common/terminal'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; export function createProfileSchemaEnums(detectedProfiles: ITerminalProfile[], extensionProfiles?: readonly IExtensionTerminalProfile[]): { - values: string[] | undefined; + values: (string | null)[] | undefined; markdownDescriptions: string[] | undefined; } { - const result = detectedProfiles.map(e => { + const result: { name: string | null; description: string }[] = [{ + name: null, + description: localize('terminalAutomaticProfile', 'Automatically detect the default') + }]; + result.push(...detectedProfiles.map(e => { return { name: e.profileName, description: createProfileDescription(e) }; - }); + })); if (extensionProfiles) { result.push(...extensionProfiles.map(extensionProfile => { return { diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index 924ee3c81b5..9bba0c3f537 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -343,8 +343,8 @@ export class PtyHostService extends Disposable implements IPtyService { this._heartbeatFirstTimeout = setTimeout(() => this._handleHeartbeatFirstTimeout(), HeartbeatConstants.BeatInterval * HeartbeatConstants.FirstWaitMultiplier); if (!this._isResponsive) { this._isResponsive = true; + this._onPtyHostResponsive.fire(); } - this._onPtyHostResponsive.fire(); } private _handleHeartbeatFirstTimeout() { @@ -358,14 +358,17 @@ export class PtyHostService extends Disposable implements IPtyService { this._heartbeatSecondTimeout = undefined; if (this._isResponsive) { this._isResponsive = false; + this._onPtyHostUnresponsive.fire(); } - this._onPtyHostUnresponsive.fire(); } private _handleUnresponsiveCreateProcess() { this._clearHeartbeatTimeouts(); this._logService.error(`No ptyHost response to createProcess after ${HeartbeatConstants.CreateProcessTimeout / 1000} seconds`); - this._onPtyHostUnresponsive.fire(); + if (this._isResponsive) { + this._isResponsive = false; + this._onPtyHostUnresponsive.fire(); + } } private _clearHeartbeatTimeouts() { diff --git a/src/vs/platform/terminal/node/windowsShellHelper.ts b/src/vs/platform/terminal/node/windowsShellHelper.ts index ba0c2ac4346..e291c2846bb 100644 --- a/src/vs/platform/terminal/node/windowsShellHelper.ts +++ b/src/vs/platform/terminal/node/windowsShellHelper.ts @@ -132,7 +132,7 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe windowsProcessTree = await import('windows-process-tree'); } this._currentRequest = new Promise<string>(resolve => { - windowsProcessTree.getProcessTree(this._rootProcessId, (tree) => { + windowsProcessTree.getProcessTree(this._rootProcessId, tree => { const name = this.traverseTree(tree); this._currentRequest = undefined; resolve(name); diff --git a/src/vs/platform/terminal/test/common/terminalProfiles.test.ts b/src/vs/platform/terminal/test/common/terminalProfiles.test.ts index c6f7520c0ed..4720794b4f8 100644 --- a/src/vs/platform/terminal/test/common/terminalProfiles.test.ts +++ b/src/vs/platform/terminal/test/common/terminalProfiles.test.ts @@ -12,8 +12,12 @@ suite('terminalProfiles', () => { suite('createProfileSchemaEnums', () => { test('should return an empty array when there are no profiles', () => { deepStrictEqual(createProfileSchemaEnums([]), { - values: [], - markdownDescriptions: [] + values: [ + null + ], + markdownDescriptions: [ + 'Automatically detect the default' + ] }); }); test('should return a single entry when there is one profile', () => { @@ -23,8 +27,14 @@ suite('terminalProfiles', () => { isDefault: true }; deepStrictEqual(createProfileSchemaEnums([profile]), { - values: ['name'], - markdownDescriptions: ['$(terminal) name\n- path: path'] + values: [ + null, + 'name' + ], + markdownDescriptions: [ + 'Automatically detect the default', + '$(terminal) name\n- path: path' + ] }); }); test('should show all profile information', () => { @@ -42,8 +52,14 @@ suite('terminalProfiles', () => { overrideName: true }; deepStrictEqual(createProfileSchemaEnums([profile]), { - values: ['name'], - markdownDescriptions: [`$(zap) name\n- path: path\n- args: ['a','b']\n- overrideName: true\n- color: terminal.ansiRed\n- env: {\"c\":\"d\",\"e\":\"f\"}`] + values: [ + null, + 'name' + ], + markdownDescriptions: [ + 'Automatically detect the default', + `$(zap) name\n- path: path\n- args: ['a','b']\n- overrideName: true\n- color: terminal.ansiRed\n- env: {\"c\":\"d\",\"e\":\"f\"}` + ] }); }); test('should return a multiple entries when there are multiple profiles', () => { @@ -58,8 +74,13 @@ suite('terminalProfiles', () => { isDefault: false }; deepStrictEqual(createProfileSchemaEnums([profile1, profile2]), { - values: ['name', 'foo'], + values: [ + null, + 'name', + 'foo' + ], markdownDescriptions: [ + 'Automatically detect the default', '$(terminal) name\n- path: path', '$(terminal) foo\n- path: bar' ] diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 2863b6f2e57..caf8e6786d8 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -212,6 +212,7 @@ export function getColorRegistry(): IColorRegistry { // ----- base colors export const foreground = registerColor('foreground', { dark: '#CCCCCC', light: '#616161', hcDark: '#FFFFFF', hcLight: '#292929' }, nls.localize('foreground', "Overall foreground color. This color is only used if not overridden by a component.")); +export const disabledForeground = registerColor('disabledForeground', { dark: '#CCCCCC80', light: '#61616180', hcDark: '#A5A5A5', hcLight: '#7F7F7F' }, nls.localize('disabledForeground', "Overall foreground for disabled elements. This color is only used if not overridden by a component.")); export const errorForeground = registerColor('errorForeground', { dark: '#F48771', light: '#A1260D', hcDark: '#F48771', hcLight: '#B5200D' }, nls.localize('errorForeground', "Overall foreground color for error messages. This color is only used if not overridden by a component.")); export const descriptionForeground = registerColor('descriptionForeground', { light: '#717171', dark: transparent(foreground, 0.7), hcDark: transparent(foreground, 0.7), hcLight: transparent(foreground, 0.7) }, nls.localize('descriptionForeground', "Foreground color for description text providing additional information, for example for a label.")); export const iconForeground = registerColor('icon.foreground', { dark: '#C5C5C5', light: '#424242', hcDark: '#FFFFFF', hcLight: '#292929' }, nls.localize('iconForeground', "The default color for icons in the workbench.")); @@ -284,11 +285,11 @@ export const scrollbarSliderActiveBackground = registerColor('scrollbarSlider.ac export const progressBarBackground = registerColor('progressBar.background', { dark: Color.fromHex('#0E70C0'), light: Color.fromHex('#0E70C0'), hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('progressBarBackground', "Background color of the progress bar that can show for long running operations.")); export const editorErrorBackground = registerColor('editorError.background', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('editorError.background', 'Background color of error text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); -export const editorErrorForeground = registerColor('editorError.foreground', { dark: '#F14C4C', light: '#E51400', hcDark: null, hcLight: '#B5200D' }, nls.localize('editorError.foreground', 'Foreground color of error squigglies in the editor.')); +export const editorErrorForeground = registerColor('editorError.foreground', { dark: '#F14C4C', light: '#E51400', hcDark: '#F48771', hcLight: '#B5200D' }, nls.localize('editorError.foreground', 'Foreground color of error squigglies in the editor.')); export const editorErrorBorder = registerColor('editorError.border', { dark: null, light: null, hcDark: Color.fromHex('#E47777').transparent(0.8), hcLight: '#B5200D' }, nls.localize('errorBorder', 'Border color of error boxes in the editor.')); export const editorWarningBackground = registerColor('editorWarning.background', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('editorWarning.background', 'Background color of warning text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); -export const editorWarningForeground = registerColor('editorWarning.foreground', { dark: '#CCA700', light: '#BF8803', hcDark: null, hcLight: '#895503' }, nls.localize('editorWarning.foreground', 'Foreground color of warning squigglies in the editor.')); +export const editorWarningForeground = registerColor('editorWarning.foreground', { dark: '#CCA700', light: '#BF8803', hcDark: '#FFD37', hcLight: '#895503' }, nls.localize('editorWarning.foreground', 'Foreground color of warning squigglies in the editor.')); export const editorWarningBorder = registerColor('editorWarning.border', { dark: null, light: null, hcDark: Color.fromHex('#FFCC00').transparent(0.8), hcLight: '#' }, nls.localize('warningBorder', 'Border color of warning boxes in the editor.')); export const editorInfoBackground = registerColor('editorInfo.background', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('editorInfo.background', 'Background color of info text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index 7e756a91cf8..b18fa04dd86 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -798,7 +798,7 @@ class WebEndpointOriginChecker { const exampleOrigin = exampleUrl.origin; const originRegExpSource = ( escapeRegExpCharacters(exampleOrigin) - .replace(uuid, '[a-zA-Z0-9\-]+') + .replace(uuid, '[a-zA-Z0-9\\-]+') ); try { const originRegExp = createRegExp(`^${originRegExpSource}$`, true, { matchCase: false }); diff --git a/src/vs/server/node/webClientServer.ts b/src/vs/server/node/webClientServer.ts index 61683e279d5..7fea1489691 100644 --- a/src/vs/server/node/webClientServer.ts +++ b/src/vs/server/node/webClientServer.ts @@ -304,7 +304,7 @@ export class WebClientServer { 'media-src \'self\';', `script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-fh3TwPMflhsEIpR8g1OYTIMVWhXTLcjQ9kh2tIpmv54=' http://${remoteAuthority};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html 'child-src \'self\';', - `frame-src 'self' https://*.vscode-webview.net data:;`, + `frame-src 'self' https://*.vscode-cdn.net data:;`, 'worker-src \'self\' data:;', 'style-src \'self\' \'unsafe-inline\';', 'connect-src \'self\' ws: wss: https:;', diff --git a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts index c4fed54577a..07a3c45afcc 100644 --- a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts +++ b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts @@ -3,11 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import { IWorkspaceEditDto, MainThreadBulkEditsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; +import { IWorkspaceEditDto, MainThreadBulkEditsShape, MainContext, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { reviveWorkspaceEditDto2 } from 'vs/workbench/api/browser/mainThreadEditors'; import { ILogService } from 'vs/platform/log/common/log'; +import { revive } from 'vs/base/common/marshalling'; +import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; +import { NotebookDto } from 'vs/workbench/api/browser/mainThreadNotebookDto'; + +export function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { + if (!data?.edits) { + return []; + } + + const result: ResourceEdit[] = []; + for (let edit of revive<IWorkspaceEditDto>(data).edits) { + if (edit._type === WorkspaceEditType.File) { + result.push(new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata)); + } else if (edit._type === WorkspaceEditType.Text) { + result.push(new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata)); + } else if (edit._type === WorkspaceEditType.Cell) { + result.push(new ResourceNotebookCellEdit(edit.resource, NotebookDto.fromCellEditOperationDto(edit.edit), edit.notebookVersionId, edit.metadata)); + } + } + return result; +} @extHostNamedCustomer(MainContext.MainThreadBulkEdits) export class MainThreadBulkEdits implements MainThreadBulkEditsShape { diff --git a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts index afb6fb751a4..12b5786f94f 100644 --- a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts @@ -7,7 +7,6 @@ import { Event } from 'vs/base/common/event'; import { IDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor, isCodeEditor, isDiffEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditor } from 'vs/editor/common/editorCommon'; import { ITextModel, shouldSynchronizeModel } from 'vs/editor/common/model'; @@ -292,7 +291,6 @@ export class MainThreadDocumentsAndEditors { @IFileService fileService: IFileService, @ITextModelService textModelResolverService: ITextModelService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, - @IBulkEditService bulkEditService: IBulkEditService, @IPaneCompositePartService paneCompositeService: IPaneCompositePartService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @@ -306,7 +304,7 @@ export class MainThreadDocumentsAndEditors { this._mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService, uriIdentityService, workingCopyFileService, pathService)); extHostContext.set(MainContext.MainThreadDocuments, this._mainThreadDocuments); - this._mainThreadEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, bulkEditService, this._editorService, this._editorGroupService, instantiationService)); + this._mainThreadEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, this._editorService, this._editorGroupService)); extHostContext.set(MainContext.MainThreadTextEditors, this._mainThreadEditors); // It is expected that the ctor of the state computer calls our `_onDelta`. diff --git a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts index 865bf0b0255..f3677aec085 100644 --- a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts +++ b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ExtHostContext, IExtHostEditorTabsShape, MainContext, IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, AnyInputDto, TabInputKind } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostContext, IExtHostEditorTabsShape, MainContext, IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, AnyInputDto, TabInputKind, TabModelOperationKind } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { EditorResourceAccessor, GroupModelChangeKind, SideBySideEditor } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; @@ -19,6 +19,8 @@ import { URI } from 'vs/base/common/uri'; import { WebviewInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInput'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; +import { isEqual } from 'vs/base/common/resources'; interface TabInfo { @@ -95,6 +97,24 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { }; } + if (editor instanceof SideBySideEditorInput && !(editor instanceof DiffEditorInput)) { + const primaryResource = editor.primary.resource; + const secondaryResource = editor.secondary.resource; + // If side by side editor with same resource on both sides treat it as a singular tab kind + if (editor.primary instanceof AbstractTextResourceEditorInput + && editor.secondary instanceof AbstractTextResourceEditorInput + && isEqual(primaryResource, secondaryResource) + && primaryResource + && secondaryResource + ) { + return { + kind: TabInputKind.TextInput, + uri: primaryResource + }; + } + return { kind: TabInputKind.UnknownInput }; + } + if (editor instanceof NotebookEditorInput) { return { kind: TabInputKind.NotebookInput, @@ -181,13 +201,18 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { * @param groupId The id of the group the tab exists in * @param editorInput The editor input represented by the tab */ - private _onDidTabLabelChange(groupId: number, editorInput: EditorInput) { + private _onDidTabLabelChange(groupId: number, editorInput: EditorInput, editorIndex: number) { const tabId = this._generateTabId(editorInput, groupId); const tabInfo = this._tabInfoLookup.get(tabId); // If tab is found patch, else rebuild if (tabInfo) { tabInfo.tab.label = editorInput.getName(); - this._proxy.$acceptTabUpdate(groupId, tabInfo.tab); + this._proxy.$acceptTabOperation({ + groupId, + index: editorIndex, + tabDto: tabInfo.tab, + kind: TabModelOperationKind.TAB_UPDATE + }); } else { console.error('Invalid model for label change, rebuilding'); this._createTabsModel(); @@ -210,15 +235,21 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { return; } const tabs = this._groupLookup.get(groupId)?.tabs; - if (tabs) { - // Splice tab into group at index editorIndex - const tabObject = this._buildTabObject(group, editorInput, editorIndex); - tabs.splice(editorIndex, 0, tabObject); - // Update lookup - this._tabInfoLookup.set(this._generateTabId(editorInput, groupId), { group, editorInput, tab: tabObject }); + if (!tabs) { + return; } - // TODO @lramos15 Switch to patching here - this._proxy.$acceptEditorTabModel(this._tabGroupModel); + // Splice tab into group at index editorIndex + const tabObject = this._buildTabObject(group, editorInput, editorIndex); + tabs.splice(editorIndex, 0, tabObject); + // Update lookup + this._tabInfoLookup.set(this._generateTabId(editorInput, groupId), { group, editorInput, tab: tabObject }); + + this._proxy.$acceptTabOperation({ + groupId, + index: editorIndex, + tabDto: tabObject, + kind: TabModelOperationKind.TAB_OPEN + }); } /** @@ -236,11 +267,21 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { } // Splice tab into group at index editorIndex const removedTab = tabs.splice(editorIndex, 1); + + // Index must no longer be valid so we return prematurely + if (removedTab.length === 0) { + return; + } + // Update lookup this._tabInfoLookup.delete(removedTab[0]?.id ?? ''); - // TODO @lramos15 Switch to patching here - this._proxy.$acceptEditorTabModel(this._tabGroupModel); + this._proxy.$acceptTabOperation({ + groupId, + index: editorIndex, + tabDto: removedTab[0], + kind: TabModelOperationKind.TAB_CLOSE + }); } /** @@ -258,7 +299,12 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { // No need to loop over as the exthost uses the most recently marked active tab activeTab.isActive = true; // Send DTO update to the exthost - this._proxy.$acceptTabUpdate(groupId, activeTab); + this._proxy.$acceptTabOperation({ + groupId, + index: editorIndex, + tabDto: activeTab, + kind: TabModelOperationKind.TAB_UPDATE + }); } @@ -269,15 +315,21 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { * @param editor The editor input represented by the tab */ private _onDidTabDirty(groupId: number, editorIndex: number, editor: EditorInput) { - const tab = this._groupLookup.get(groupId)?.tabs[editorIndex]; - // Something wrong with the model staate so we rebuild - if (!tab) { + const tabId = this._generateTabId(editor, groupId); + const tabInfo = this._tabInfoLookup.get(tabId); + // Something wrong with the model state so we rebuild + if (!tabInfo) { console.error('Invalid model for dirty change, rebuilding'); this._createTabsModel(); return; } - tab.isDirty = editor.isDirty(); - this._proxy.$acceptTabUpdate(groupId, tab); + tabInfo.tab.isDirty = editor.isDirty(); + this._proxy.$acceptTabOperation({ + groupId, + index: editorIndex, + tabDto: tabInfo.tab, + kind: TabModelOperationKind.TAB_UPDATE + }); } /** @@ -299,7 +351,12 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { } // Whether or not the tab has the pin icon (internally it's called sticky) tab.isPinned = group.isSticky(editorIndex); - this._proxy.$acceptTabUpdate(groupId, tab); + this._proxy.$acceptTabOperation({ + groupId, + index: editorIndex, + tabDto: tab, + kind: TabModelOperationKind.TAB_UPDATE + }); } /** @@ -319,9 +376,14 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { this._createTabsModel(); return; } - // Whether or not the tab has the pin icon (internally it's called sticky) + // Whether or not the tab has the pin icon (internally it's called pinned) tab.isPreview = !group.isPinned(editorIndex); - this._proxy.$acceptTabUpdate(groupId, tab); + this._proxy.$acceptTabOperation({ + kind: TabModelOperationKind.TAB_UPDATE, + groupId, + tabDto: tab, + index: editorIndex + }); } /** @@ -392,8 +454,8 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { return; } case GroupModelChangeKind.EDITOR_LABEL: - if (event.editor !== undefined) { - this._onDidTabLabelChange(event.groupId, event.editor); + if (event.editor !== undefined && event.editorIndex !== undefined) { + this._onDidTabLabelChange(event.groupId, event.editor, event.editorIndex); break; } case GroupModelChangeKind.EDITOR_OPEN: @@ -473,7 +535,7 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { return; } - async $closeTab(tabIds: string[], preserveFocus?: boolean): Promise<void> { + async $closeTab(tabIds: string[], preserveFocus?: boolean): Promise<boolean> { const groups: Map<IEditorGroup, EditorInput[]> = new Map(); for (const tabId of tabIds) { const tabInfo = this._tabInfoLookup.get(tabId); @@ -492,9 +554,12 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { } } // Loop over keys of the groups map and call closeEditors + let results: boolean[] = []; for (const [group, editors] of groups) { - await group.closeEditors(editors, { preserveFocus }); + results.push(await group.closeEditors(editors, { preserveFocus })); } + // TODO @jrieken This isn't quite right how can we say true for some but not others? + return results.every(result => result); } //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 441ab32a640..80babfd1c70 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -7,7 +7,6 @@ import { disposed } from 'vs/base/common/errors'; import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { equals as objectEquals } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IRange } from 'vs/editor/common/core/range'; import { ISelection } from 'vs/editor/common/core/selection'; @@ -15,46 +14,19 @@ import { IDecorationOptions, IDecorationRenderOptions } from 'vs/editor/common/e import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ITextEditorOptions, IResourceEditorInput, EditorActivation, EditorResolution } from 'vs/platform/editor/common/editor'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { MainThreadTextEditor } from 'vs/workbench/api/browser/mainThreadEditor'; -import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType, IWorkspaceEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType } from 'vs/workbench/api/common/extHost.protocol'; import { editorGroupToColumn, columnToEditorGroup, EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { revive } from 'vs/base/common/marshalling'; -import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { NotebookDto } from 'vs/workbench/api/browser/mainThreadNotebookDto'; import { ILineChange } from 'vs/editor/common/diff/diffComputer'; import { IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { IEditorControl } from 'vs/workbench/common/editor'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { DataTransferConverter } from 'vs/workbench/api/common/shared/dataTransfer'; -import { IPosition } from 'vs/editor/common/core/position'; -import { IDataTransfer, IDataTransferItem } from 'vs/workbench/common/dnd'; -import { extractEditorsDropData } from 'vs/workbench/browser/dnd'; -import { Mimes } from 'vs/base/common/mime'; -import { distinct } from 'vs/base/common/arrays'; - -export function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { - if (!data?.edits) { - return []; - } - - const result: ResourceEdit[] = []; - for (let edit of revive<IWorkspaceEditDto>(data).edits) { - if (edit._type === WorkspaceEditType.File) { - result.push(new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata)); - } else if (edit._type === WorkspaceEditType.Text) { - result.push(new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata)); - } else if (edit._type === WorkspaceEditType.Cell) { - result.push(new ResourceNotebookCellEdit(edit.resource, NotebookDto.fromCellEditOperationDto(edit.edit), edit.notebookVersionId, edit.metadata)); - } - } - return result; -} export interface IMainThreadEditorLocator { getEditor(id: string): MainThreadTextEditor | undefined; @@ -72,16 +44,13 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { private _textEditorsListenersMap: { [editorId: string]: IDisposable[] }; private _editorPositionData: ITextEditorPositionData | null; private _registeredDecorationTypes: { [decorationType: string]: boolean }; - private readonly _dropIntoEditorListeners = new Map<ICodeEditor, IDisposable>(); constructor( private readonly _editorLocator: IMainThreadEditorLocator, extHostContext: IExtHostContext, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IBulkEditService private readonly _bulkEditService: IBulkEditService, @IEditorService private readonly _editorService: IEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { this._instanceId = String(++MainThreadTextEditors.INSTANCE_COUNT); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditors); @@ -93,25 +62,10 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { this._toDispose.add(this._editorGroupService.onDidRemoveGroup(() => this._updateActiveAndVisibleTextEditors())); this._toDispose.add(this._editorGroupService.onDidMoveGroup(() => this._updateActiveAndVisibleTextEditors())); - const registerDropListenerOnEditor = (editor: ICodeEditor) => { - this._dropIntoEditorListeners.get(editor)?.dispose(); - this._dropIntoEditorListeners.set(editor, editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.event))); - }; - - this._toDispose.add(_codeEditorService.onCodeEditorAdd(registerDropListenerOnEditor)); - - this._toDispose.add(_codeEditorService.onCodeEditorRemove(editor => { - this._dropIntoEditorListeners.get(editor)?.dispose(); - })); - - for (const editor of this._codeEditorService.listCodeEditors()) { - registerDropListenerOnEditor(editor); - } - this._registeredDecorationTypes = Object.create(null); } - public dispose(): void { + dispose(): void { Object.keys(this._textEditorsListenersMap).forEach((editorId) => { dispose(this._textEditorsListenersMap[editorId]); }); @@ -120,8 +74,6 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { for (let decorationType in this._registeredDecorationTypes) { this._codeEditorService.removeDecorationType(decorationType); } - dispose(this._dropIntoEditorListeners.values()); - this._dropIntoEditorListeners.clear(); this._registeredDecorationTypes = Object.create(null); } @@ -161,47 +113,6 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return result; } - private async onDropIntoEditor(editor: ICodeEditor, position: IPosition, dragEvent: DragEvent) { - if (!dragEvent.dataTransfer) { - return; - } - const id = this._editorLocator.getIdOfCodeEditor(editor); - if (typeof id !== 'string') { - return; - } - - const textEditorDataTransfer: IDataTransfer = new Map<string, IDataTransferItem>(); - for (const item of dragEvent.dataTransfer.items) { - if (item.kind === 'string') { - const type = item.type; - const asStringValue = new Promise<string>(resolve => item.getAsString(resolve)); - textEditorDataTransfer.set(type, { - asString: () => asStringValue, - value: undefined - }); - } - } - - if (!textEditorDataTransfer.has(Mimes.uriList.toLowerCase())) { - const editorData = (await this._instantiationService.invokeFunction(extractEditorsDropData, dragEvent)) - .filter(input => input.resource) - .map(input => input.resource!.toString()); - - if (editorData.length) { - const str = distinct(editorData).join('\n'); - textEditorDataTransfer.set(Mimes.uriList.toLowerCase(), { - asString: () => Promise.resolve(str), - value: undefined - }); - } - } - - if (textEditorDataTransfer.size > 0) { - const dataTransferDto = await DataTransferConverter.toDataTransferDTO(textEditorDataTransfer); - return this._proxy.$textEditorHandleDrop(id, position, dataTransferDto); - } - } - // --- from extension host process async $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Promise<string | undefined> { @@ -309,11 +220,6 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return Promise.resolve(editor.applyEdits(modelVersionId, edits, opts)); } - $tryApplyWorkspaceEdit(dto: IWorkspaceEditDto): Promise<boolean> { - const edits = reviveWorkspaceEditDto2(dto); - return this._bulkEditService.apply(edits).then(() => true, _err => false); - } - $tryInsertSnippet(id: string, modelVersionId: number, template: string, ranges: readonly IRange[], opts: IUndoStopOptions): Promise<boolean> { const editor = this._editorLocator.getEditor(id); if (!editor) { diff --git a/src/vs/workbench/api/browser/mainThreadFileSystem.ts b/src/vs/workbench/api/browser/mainThreadFileSystem.ts index 48c189d814a..bbf3476b101 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystem.ts +++ b/src/vs/workbench/api/browser/mainThreadFileSystem.ts @@ -165,17 +165,22 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { $watch(extensionId: string, session: number, resource: UriComponents, opts: IWatchOptions): void { const uri = URI.revive(resource); - - // refuse to watch anything that is already watched via - // our workspace watchers - if (this._contextService.isInsideWorkspace(uri)) { + const isInsideWorkspace = this._contextService.isInsideWorkspace(uri); + + // Refuse to watch anything that is already watched via + // our workspace watchers in case the request is a + // recursive file watcher. + // Still allow for non-recursive watch requests as a way + // to bypass configured exlcude rules though + // (see https://github.com/microsoft/vscode/issues/146066) + if (isInsideWorkspace && opts.recursive) { this._logService.trace(`MainThreadFileSystem#$watch(): ignoring request to start watching because path is inside workspace (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`); return; } this._logService.trace(`MainThreadFileSystem#$watch(): request to start watching (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`); - // automatically add `files.watcherExclude` patterns when watching + // Automatically add `files.watcherExclude` patterns when watching // recursively to give users a chance to configure exclude rules // for reducing the overhead of watching recursively if (opts.recursive) { @@ -189,6 +194,35 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { } } + // Non-recursive watching inside the workspace will overlap with + // our standard workspace watchers. To prevent duplicate events, + // we only want to include events for files that are otherwise + // excluded via `files.watcherExclude`. As such, we configure + // to include each configured exclude pattern so that only those + // events are reported that are otherwise excluded. + else if (isInsideWorkspace) { + const config = this._configurationService.getValue<IFilesConfiguration>(); + if (config.files?.watcherExclude) { + for (const key in config.files.watcherExclude) { + if (config.files.watcherExclude[key] === true) { + if (!opts.includes) { + opts.includes = []; + } + + opts.includes.push(key); + } + } + } + + // Still ignore watch request if there are actually no configured + // exclude rules, because in that case our default recursive watcher + // should be able to take care of all events. + if (!opts.includes || opts.includes.length === 0) { + this._logService.trace(`MainThreadFileSystem#$watch(): ignoring request to start watching because path is inside workspace and no excludes are configured (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`); + return; + } + } + const subscription = this._fileService.watch(uri, opts); this._watches.set(session, subscription); } diff --git a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts index 756fb3bc722..da8611e4f5b 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts +++ b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts @@ -8,10 +8,8 @@ import { FileOperation, IFileService } from 'vs/platform/files/common/files'; import { extHostCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { ExtHostContext } from '../common/extHost.protocol'; import { localize } from 'vs/nls'; -import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkingCopyFileOperationParticipant, IWorkingCopyFileService, SourceTargetPair, IFileOperationUndoRedoInfo } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; -import { reviveWorkspaceEditDto2 } from 'vs/workbench/api/browser/mainThreadEditors'; +import { reviveWorkspaceEditDto2 } from 'vs/workbench/api/browser/mainThreadBulkEdits'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { raceCancellation } from 'vs/base/common/async'; @@ -195,15 +193,3 @@ registerAction2(class ResetMemento extends Action2 { accessor.get(IStorageService).remove(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.GLOBAL); } }); - - -Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({ - id: 'files', - properties: { - 'files.participants.timeout': { - type: 'number', - default: 60000, - markdownDescription: localize('files.participants.timeout', "Timeout in milliseconds after which file participants for create, rename, and delete are cancelled. Use `0` to disable participants."), - } - } -}); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index f8346dcf668..be97123b3c9 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -3,17 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { distinct } from 'vs/base/common/arrays'; import { Emitter, Event } from 'vs/base/common/event'; import { ITextModel } from 'vs/editor/common/model'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import * as languages from 'vs/editor/common/languages'; import * as search from 'vs/workbench/contrib/search/common/search'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { Position as EditorPosition } from 'vs/editor/common/core/position'; +import { IPosition, Position as EditorPosition } from 'vs/editor/common/core/position'; import { Range as EditorRange, IRange } from 'vs/editor/common/core/range'; import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, ILocationLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField, ICodeActionProviderMetadataDto, ILanguageWordDefinitionDto, IdentifiableInlineCompletions, IdentifiableInlineCompletion, ITypeHierarchyItemDto, IInlayHintDto } from '../common/extHost.protocol'; -import { ILanguageConfigurationService, LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/languages/languageConfiguration'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -27,28 +28,40 @@ import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticToken import { revive } from 'vs/base/common/marshalling'; import { CancellationError } from 'vs/base/common/errors'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { Mimes } from 'vs/base/common/mime'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { performSnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { DataTransferConverter } from 'vs/workbench/api/common/shared/dataTransfer'; +import { extractEditorsDropData } from 'vs/workbench/browser/dnd'; +import { IDataTransfer, IDataTransferItem } from 'vs/workbench/common/dnd'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) -export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape { +export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape { private readonly _proxy: ExtHostLanguageFeaturesShape; - private readonly _languageService: ILanguageService; private readonly _registrations = new Map<number, IDisposable>(); + private readonly _dropIntoEditorListeners = new Map<ICodeEditor, IDisposable>(); + constructor( extHostContext: IExtHostContext, - @ILanguageService languageService: ILanguageService, - @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService, + @ILanguageService private readonly _languageService: ILanguageService, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { + super(); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostLanguageFeatures); - this._languageService = languageService; if (this._languageService) { const updateAllWordDefinitions = () => { let wordDefinitionDtos: ILanguageWordDefinitionDto[] = []; - for (const languageId of languageService.getRegisteredLanguageIds()) { - const wordDefinition = languageConfigurationService.getLanguageConfiguration(languageId).getWordDefinition(); + for (const languageId of _languageService.getRegisteredLanguageIds()) { + const wordDefinition = this._languageConfigurationService.getLanguageConfiguration(languageId).getWordDefinition(); wordDefinitionDtos.push({ languageId: languageId, regexSource: wordDefinition.source, @@ -57,11 +70,11 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha } this._proxy.$setWordDefinitions(wordDefinitionDtos); }; - languageConfigurationService.onDidChange((e) => { + this._languageConfigurationService.onDidChange((e) => { if (!e.languageId) { updateAllWordDefinitions(); } else { - const wordDefinition = languageConfigurationService.getLanguageConfiguration(e.languageId).getWordDefinition(); + const wordDefinition = this._languageConfigurationService.getLanguageConfiguration(e.languageId).getWordDefinition(); this._proxy.$setWordDefinitions([{ languageId: e.languageId, regexSource: wordDefinition.source, @@ -71,13 +84,36 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha }); updateAllWordDefinitions(); } + + if (this._codeEditorService) { + const registerDropListenerOnEditor = (editor: ICodeEditor) => { + this._dropIntoEditorListeners.get(editor)?.dispose(); + this._dropIntoEditorListeners.set(editor, editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.event))); + }; + + this._register(this._codeEditorService.onCodeEditorAdd(registerDropListenerOnEditor)); + + this._register(this._codeEditorService.onCodeEditorRemove(editor => { + this._dropIntoEditorListeners.get(editor)?.dispose(); + this._dropIntoEditorListeners.delete(editor); + })); + + for (const editor of this._codeEditorService.listCodeEditors()) { + registerDropListenerOnEditor(editor); + } + } } - dispose(): void { + override dispose(): void { for (const registration of this._registrations.values()) { registration.dispose(); } this._registrations.clear(); + + dispose(this._dropIntoEditorListeners.values()); + this._dropIntoEditorListeners.clear(); + + super.dispose(); } $unregister(handle: number): void { @@ -811,7 +847,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha } if (this._languageService.isRegisteredLanguageId(languageId)) { - this._registrations.set(handle, LanguageConfigurationRegistry.register(languageId, configuration, 100)); + this._registrations.set(handle, this._languageConfigurationService.register(languageId, configuration, 100)); } } @@ -852,6 +888,69 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha })); } + + // --- document drop Edits + + $registerDocumentOnDropProvider(handle: number, selector: IDocumentFilterDto[]): void { + this._registrations.set(handle, this._languageFeaturesService.documentOnDropEditProvider.register(selector, { + provideDocumentOnDropEdits: async (model, position, dataTransfer, token) => { + const dataTransferDto = await DataTransferConverter.toDataTransferDTO(dataTransfer); + return this._proxy.$provideDocumentOnDropEdits(handle, model.uri, position, dataTransferDto, token); + } + })); + } + + private async onDropIntoEditor(editor: ICodeEditor, position: IPosition, dragEvent: DragEvent) { + if (!dragEvent.dataTransfer || !editor.hasModel()) { + return; + } + + const model = editor.getModel(); + const modelVersionNow = model.getVersionId(); + + const textEditorDataTransfer: IDataTransfer = new Map<string, IDataTransferItem>(); + for (const item of dragEvent.dataTransfer.items) { + if (item.kind === 'string') { + const type = item.type; + const asStringValue = new Promise<string>(resolve => item.getAsString(resolve)); + textEditorDataTransfer.set(type, { + asString: () => asStringValue, + value: undefined + }); + } + } + + if (!textEditorDataTransfer.has(Mimes.uriList.toLowerCase())) { + const editorData = (await this._instantiationService.invokeFunction(extractEditorsDropData, dragEvent)) + .filter(input => input.resource) + .map(input => input.resource!.toString()); + + if (editorData.length) { + const str = distinct(editorData).join('\n'); + textEditorDataTransfer.set(Mimes.uriList.toLowerCase(), { + asString: () => Promise.resolve(str), + value: undefined + }); + } + } + + if (textEditorDataTransfer.size === 0) { + return; + } + + const ordered = this._languageFeaturesService.documentOnDropEditProvider.ordered(model); + for (const provider of ordered) { + const edit = await provider.provideDocumentOnDropEdits(model, position, textEditorDataTransfer, CancellationToken.None); + if (editor.getModel().getVersionId() !== modelVersionNow) { + return; + } + + if (edit) { + performSnippetEdit(editor, edit); + return; + } + } + } } export class MainThreadDocumentSemanticTokensProvider implements languages.DocumentSemanticTokensProvider { diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index db80199bc2f..b22dce389de 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -12,11 +12,13 @@ import { NotebookDto } from 'vs/workbench/api/browser/mainThreadNotebookDto'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { INotebookCellStatusBarItemProvider, INotebookContributionData, NotebookData as NotebookData, NotebookExtensionDescription, TransientCellMetadata, TransientDocumentMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookContentProvider, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { INotebookContentProvider, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { ExtHostContext, ExtHostNotebookShape, MainContext, MainThreadNotebookShape } from '../common/extHost.protocol'; import { ILogService } from 'vs/platform/log/common/log'; import { StopWatch } from 'vs/base/common/stopwatch'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { assertType } from 'vs/base/common/types'; @extHostNamedCustomer(MainContext.MainThreadNotebook) export class MainThreadNotebooks implements MainThreadNotebookShape { @@ -184,3 +186,36 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { } } } + +CommandsRegistry.registerCommand('_executeDataToNotebook', async (accessor, ...args) => { + + const [notebookType, bytes] = args; + assertType(typeof notebookType === 'string', 'string'); + assertType(bytes instanceof VSBuffer, 'VSBuffer'); + + const notebookService = accessor.get(INotebookService); + const info = await notebookService.withNotebookDataProvider(notebookType); + if (!(info instanceof SimpleNotebookProviderInfo)) { + return; + } + + const dto = await info.serializer.dataToNotebook(bytes); + return NotebookDto.toNotebookDataDto(dto); +}); + +CommandsRegistry.registerCommand('_executeNotebookToData', async (accessor, ...args) => { + + const [notebookType, dto] = args; + assertType(typeof notebookType === 'string', 'string'); + assertType(typeof dto === 'object', 'NotebookDataDto'); + + const notebookService = accessor.get(INotebookService); + const info = await notebookService.withNotebookDataProvider(notebookType); + if (!(info instanceof SimpleNotebookProviderInfo)) { + return; + } + + const data = NotebookDto.fromNotebookDataDto(dto); + const bytes = await info.serializer.notebookToData(data); + return bytes; +}); diff --git a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts index d48cc2f305b..c18d0ac258a 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts @@ -7,7 +7,6 @@ import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { URI, UriComponents } from 'vs/base/common/uri'; import { BoundModelReferenceCollection } from 'vs/workbench/api/browser/mainThreadDocuments'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; @@ -33,10 +32,14 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookDocuments); this._modelReferenceCollection = new BoundModelReferenceCollection(this._uriIdentityService.extUri); - // forward dirty and save events this._disposables.add(this._notebookEditorModelResolverService.onDidChangeDirty(model => this._proxy.$acceptDirtyStateChanged(model.resource, model.isDirty()))); this._disposables.add(this._notebookEditorModelResolverService.onDidSaveNotebook(e => this._proxy.$acceptModelSaved(e))); + + // when a conflict is going to happen RELEASE references that are held by extensions + this._disposables.add(_notebookEditorModelResolverService.onWillFailWithConflict(e => { + this._modelReferenceCollection.remove(e.resource); + })); } dispose(): void { @@ -62,7 +65,7 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS case NotebookCellsChangeType.ModelChange: eventDto.rawEvents.push({ kind: e.kind, - changes: e.changes.map(diff => [diff[0], diff[1], diff[2].map(cell => NotebookDto.toNotebookCellDto(cell as NotebookCellTextModel))] as [number, number, NotebookCellDto[]]) + changes: e.changes.map(diff => [diff[0], diff[1], diff[2].map(cell => NotebookDto.toNotebookCellDto(cell))] as [number, number, NotebookCellDto[]]) }); break; case NotebookCellsChangeType.Move: @@ -89,7 +92,7 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS append: e.append }); break; - case NotebookCellsChangeType.ChangeLanguage: + case NotebookCellsChangeType.ChangeCellLanguage: case NotebookCellsChangeType.ChangeCellContent: case NotebookCellsChangeType.ChangeCellMetadata: case NotebookCellsChangeType.ChangeCellInternalMetadata: @@ -109,10 +112,6 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS this._notebookEditorModelResolverService.isDirty(textModel.uri), hasDocumentMetadataChangeEvent ? textModel.metadata : undefined ); - - if (hasDocumentMetadataChangeEvent) { - this._proxy.$acceptDocumentPropertiesChanged(textModel.uri, { metadata: textModel.metadata }); - } })); this._documentEventListenersMapping.set(textModel.uri, disposableStore); diff --git a/src/vs/workbench/api/browser/mainThreadNotebookDto.ts b/src/vs/workbench/api/browser/mainThreadNotebookDto.ts index b9a4cf129ce..8f5d53fd444 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookDto.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookDto.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { ICellExecuteUpdate, ICellExecutionComplete } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; @@ -79,7 +78,7 @@ export namespace NotebookDto { }; } - export function toNotebookCellDto(cell: NotebookCellTextModel): extHostProtocol.NotebookCellDto { + export function toNotebookCellDto(cell: notebookCommon.ICell): extHostProtocol.NotebookCellDto { return { handle: cell.handle, uri: cell.uri, diff --git a/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts b/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts index 61139ea2144..9254a9e2fb4 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts @@ -112,7 +112,7 @@ export class MainThreadNotebookEditors implements MainThreadNotebookEditorsShape return false; } //todo@jrieken use proper selection logic! - return editor.textModel.applyEdits(cellEdits.map(NotebookDto.fromCellEditOperationDto), true, undefined, () => undefined, undefined); + return editor.textModel.applyEdits(cellEdits.map(NotebookDto.fromCellEditOperationDto), true, undefined, () => undefined, undefined, true); } async $tryShowNotebookDocument(resource: UriComponents, viewType: string, options: INotebookDocumentShowOptions): Promise<string> { diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index dc7b4e941a9..46febd36991 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { flatten, isNonEmptyArray } from 'vs/base/common/arrays'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -43,7 +43,7 @@ abstract class MainThreadKernel implements INotebookKernel { } public get preloadProvides() { - return flatten(this.preloads.map(p => p.provides)); + return this.preloads.map(p => p.provides).flat(); } constructor(data: INotebookKernelDto2, private _languageService: ILanguageService) { diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index aebea377c3a..01fd391e965 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -11,7 +11,7 @@ import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; -import { ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 920a7527d62..482e97935c5 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -5,7 +5,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem, ITreeViewDragAndDropController } from 'vs/workbench/common/views'; +import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem, ITreeViewDragAndDropController, IViewBadge } from 'vs/workbench/common/views'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { distinct } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -102,6 +102,15 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie } } + $setBadge(treeViewId: string, badge: IViewBadge | undefined): void { + this.logService.trace('MainThreadTreeViews#$setBadge', treeViewId, badge?.value, badge?.tooltip); + + const viewer = this.getTreeView(treeViewId); + if (viewer) { + viewer.badge = badge; + } + } + private async reveal(treeView: ITreeView, dataProvider: TreeViewDataProvider, itemIn: ITreeItem, parentChain: ITreeItem[], options: IRevealOptions): Promise<void> { options = options ? options : { select: false, focus: false }; const select = isUndefinedOrNull(options.select) ? false : options.select; diff --git a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts index bc4cdd6781b..ef417bf044c 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts @@ -8,6 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { MainThreadWebviews, reviveWebviewExtension } from 'vs/workbench/api/browser/mainThreadWebviews'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; +import { IViewBadge } from 'vs/workbench/common/views'; import { IWebviewViewService, WebviewView } from 'vs/workbench/contrib/webviewView/browser/webviewViewService'; import { IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -48,6 +49,11 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc webviewView.description = value; } + public $setWebviewViewBadge(handle: string, badge: IViewBadge | undefined): void { + const webviewView = this.getWebviewView(handle); + webviewView.badge = badge; + } + public $show(handle: extHostProtocol.WebviewHandle, preserveFocus: boolean): void { const webviewView = this.getWebviewView(handle); webviewView.show(preserveFocus); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index ef332ceaa2d..e59da75b06d 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -153,7 +153,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadBulkEdits))); const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extensionStoragePaths)); - const extHostNotebookDocuments = rpcProtocol.set(ExtHostContext.ExtHostNotebookDocuments, new ExtHostNotebookDocuments(extHostLogService, extHostNotebook)); + const extHostNotebookDocuments = rpcProtocol.set(ExtHostContext.ExtHostNotebookDocuments, new ExtHostNotebookDocuments(extHostNotebook)); const extHostNotebookEditors = rpcProtocol.set(ExtHostContext.ExtHostNotebookEditors, new ExtHostNotebookEditors(extHostLogService, rpcProtocol, extHostNotebook)); const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostCommands, extHostLogService)); const extHostNotebookRenderers = rpcProtocol.set(ExtHostContext.ExtHostNotebookRenderers, new ExtHostNotebookRenderers(rpcProtocol, extHostNotebook)); @@ -532,6 +532,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, createLanguageStatusItem(id: string, selector: vscode.DocumentSelector): vscode.LanguageStatusItem { return extHostLanguages.createLanguageStatusItem(extension, id, selector); + }, + registerDocumentOnDropProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentOnDropProvider): vscode.Disposable { + checkProposedApiEnabled(extension, 'textEditorDrop'); + return extHostLanguageFeatures.registerDocumentOnDropProvider(extension, selector, provider); } }; @@ -872,10 +876,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onWillSaveTextDocument: (listener, thisArgs?, disposables?) => { return extHostDocumentSaveParticipant.getOnWillSaveTextDocumentEvent(extension)(listener, thisArgs, disposables); }, - onWillDropOnTextEditor: (listener, thisArgs?, disposables?) => { - checkProposedApiEnabled(extension, 'textEditorDrop'); - return extHostEditors.onWillDropOnTextEditor(listener, thisArgs, disposables); - }, get notebookDocuments(): vscode.NotebookDocument[] { return extHostNotebook.notebookDocuments.map(d => d.apiNotebook); }, @@ -1118,10 +1118,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerNotebookCellStatusBarItemProvider: (notebookType: string, provider: vscode.NotebookCellStatusBarItemProvider) => { return extHostNotebook.registerNotebookCellStatusBarItemProvider(extension, notebookType, provider); }, - get onDidSaveNotebookDocument(): Event<vscode.NotebookDocument> { - checkProposedApiEnabled(extension, 'notebookEditor'); - return extHostNotebookDocuments.onDidSaveNotebookDocument; - }, createNotebookEditorDecorationType(options: vscode.NotebookDecorationRenderOptions): vscode.NotebookEditorDecorationType { checkProposedApiEnabled(extension, 'notebookEditorDecorationType'); return extHostNotebookEditors.createNotebookEditorDecorationType(options); @@ -1129,29 +1125,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I createRendererMessaging(rendererId) { return extHostNotebookRenderers.createRendererMessaging(extension, rendererId); }, - onDidChangeNotebookDocumentMetadata(listener, thisArgs?, disposables?) { - checkProposedApiEnabled(extension, 'notebookEditor'); - return extHostNotebookDocuments.onDidChangeNotebookDocumentMetadata(listener, thisArgs, disposables); - }, - onDidChangeNotebookCells(listener, thisArgs?, disposables?) { - checkProposedApiEnabled(extension, 'notebookEditor'); - return extHostNotebook.onDidChangeNotebookCells(listener, thisArgs, disposables); - }, onDidChangeNotebookCellExecutionState(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension, 'notebookCellExecutionState'); return extHostNotebookKernels.onDidChangeNotebookCellExecutionState(listener, thisArgs, disposables); }, - onDidChangeCellOutputs(listener, thisArgs?, disposables?) { - checkProposedApiEnabled(extension, 'notebookEditor'); - return extHostNotebook.onDidChangeCellOutputs(listener, thisArgs, disposables); - }, - onDidChangeCellMetadata(listener, thisArgs?, disposables?) { - checkProposedApiEnabled(extension, 'notebookEditor'); - return extHostNotebook.onDidChangeCellMetadata(listener, thisArgs, disposables); - }, createConcatTextDocument(notebook, selector) { checkProposedApiEnabled(extension, 'notebookConcatTextDocument'); - return new ExtHostNotebookConcatDocument(extHostNotebook, extHostDocuments, notebook, selector); + return new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook, selector); }, }; @@ -1263,6 +1243,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I SignatureHelpTriggerKind: extHostTypes.SignatureHelpTriggerKind, SignatureInformation: extHostTypes.SignatureInformation, SnippetString: extHostTypes.SnippetString, + SnippetTextEdit: extHostTypes.SnippetTextEdit, SourceBreakpoint: extHostTypes.SourceBreakpoint, StandardTokenType: extHostTypes.StandardTokenType, StatusBarAlignment: extHostTypes.StatusBarAlignment, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 29b3e622c57..c33567f8d99 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -46,7 +46,7 @@ import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/works import * as tasks from 'vs/workbench/api/common/shared/tasks'; import { DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer'; import { SaveReason } from 'vs/workbench/common/editor'; -import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views'; +import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -57,7 +57,7 @@ import { OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/outp import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; import { IWorkspaceSymbol } from 'vs/workbench/contrib/search/common/search'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ISerializedTestResults, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, RunTestForControllerRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testCollection'; +import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ISerializedTestResults, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, RunTestForControllerRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { InternalTimelineOptions, Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/workbench/services/authentication/common/authentication'; @@ -262,6 +262,7 @@ export interface MainThreadTreeViewsShape extends IDisposable { $reveal(treeViewId: string, itemInfo: { item: ITreeItem; parentChain: ITreeItem[] } | undefined, options: IRevealOptions): Promise<void>; $setMessage(treeViewId: string, message: string): void; $setTitle(treeViewId: string, title: string, description: string | undefined): void; + $setBadge(treeViewId: string, badge: IViewBadge | undefined): void; } export interface MainThreadDownloadServiceShape extends IDisposable { @@ -389,6 +390,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerSelectionRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerCallHierarchyProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerTypeHierarchyProvider(handle: number, selector: IDocumentFilterDto[]): void; + $registerDocumentOnDropProvider(handle: number, selector: IDocumentFilterDto[]): void; $setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void; } @@ -620,6 +622,12 @@ export const enum TabInputKind { TerminalEditorInput } +export const enum TabModelOperationKind { + TAB_OPEN, + TAB_CLOSE, + TAB_UPDATE +} + export interface UnknownInputDto { kind: TabInputKind.UnknownInput; } @@ -668,7 +676,7 @@ export type AnyInputDto = UnknownInputDto | TextInputDto | TextDiffInputDto | No export interface MainThreadEditorTabsShape extends IDisposable { // manage tabs: move, close, rearrange etc $moveTab(tabId: string, index: number, viewColumn: EditorGroupColumn, preserveFocus?: boolean): void; - $closeTab(tabIds: string[], preserveFocus?: boolean): Promise<void>; + $closeTab(tabIds: string[], preserveFocus?: boolean): Promise<boolean>; } export interface IEditorTabGroupDto { @@ -680,6 +688,14 @@ export interface IEditorTabGroupDto { groupId: number; } +export interface TabOperation { + readonly kind: TabModelOperationKind.TAB_OPEN | TabModelOperationKind.TAB_CLOSE | TabModelOperationKind.TAB_UPDATE; + // TODO @lramos15 Possibly get rid of index for tab update, it's only needed for open and close + readonly index: number; + readonly tabDto: IEditorTabDto; + readonly groupId: number; +} + export interface IEditorTabDto { id: string; label: string; @@ -696,8 +712,8 @@ export interface IExtHostEditorTabsShape { $acceptEditorTabModel(tabGroups: IEditorTabGroupDto[]): void; // Only when group property changes (not the tabs inside) $acceptTabGroupUpdate(groupDto: IEditorTabGroupDto): void; - // Only when tab property changes - $acceptTabUpdate(groupId: number, tabDto: IEditorTabDto): void; + // When a tab is added, removed, or updated + $acceptTabOperation(operation: TabOperation): void; } //#endregion @@ -820,6 +836,7 @@ export interface MainThreadWebviewViewsShape extends IDisposable { $setWebviewViewTitle(handle: WebviewHandle, value: string | undefined): void; $setWebviewViewDescription(handle: WebviewHandle, value: string | undefined): void; + $setWebviewViewBadge(handle: WebviewHandle, badge: IViewBadge | undefined): void; $show(handle: WebviewHandle, preserveFocus: boolean): void; } @@ -1313,7 +1330,6 @@ export interface ISelectionChangeEvent { export interface ExtHostEditorsShape { $acceptEditorPropertiesChanged(id: string, props: IEditorPropertiesChangeData): void; $acceptEditorPositionData(data: ITextEditorPositionData): void; - $textEditorHandleDrop(id: string, position: IPosition, dataTransferDto: DataTransferDTO): Promise<void>; } export interface IDocumentsAndEditorsDelta { @@ -1718,6 +1734,7 @@ export interface ExtHostLanguageFeaturesShape { $provideTypeHierarchySupertypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise<ITypeHierarchyItemDto[] | undefined>; $provideTypeHierarchySubtypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise<ITypeHierarchyItemDto[] | undefined>; $releaseTypeHierarchy(handle: number, sessionId: string): void; + $provideDocumentOnDropEdits(handle: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise<Dto<languages.SnippetTextEdit> | undefined>; } export interface ExtHostQuickOpenShape { @@ -2064,7 +2081,6 @@ export interface ExtHostNotebookDocumentsShape { $acceptModelChanged(uriComponents: UriComponents, event: SerializableObjectWithBuffers<NotebookCellsChangedEventDto>, isDirty: boolean, newMetadata?: notebookCommon.NotebookDocumentMetadata): void; $acceptDirtyStateChanged(uriComponents: UriComponents, isDirty: boolean): void; $acceptModelSaved(uriComponents: UriComponents): void; - $acceptDocumentPropertiesChanged(uriComponents: UriComponents, data: INotebookDocumentPropertiesChangeData): void; } export type INotebookEditorViewColumnInfo = Record<string, number>; diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 2ea50c6e796..c8fb9cf0ed6 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -21,7 +21,7 @@ import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ISelection } from 'vs/editor/common/core/selection'; -import { TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; +import { TestItemImpl } from 'vs/workbench/api/common/extHostTestItem'; import { VSBuffer } from 'vs/base/common/buffer'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -189,6 +189,9 @@ export class ExtHostCommands implements ExtHostCommandsShape { } else if (value instanceof Uint8Array) { hasBuffers = true; return VSBuffer.wrap(value); + } else if (value instanceof VSBuffer) { + hasBuffers = true; + return value; } if (!Array.isArray(value)) { return value; diff --git a/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts index 4f99d7526bd..cbd2effae0c 100644 --- a/src/vs/workbench/api/common/extHostEditorTabs.ts +++ b/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -5,7 +5,7 @@ import type * as vscode from 'vscode'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; -import { IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext, MainThreadEditorTabsShape, TabInputKind } from 'vs/workbench/api/common/extHost.protocol'; +import { IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TabOperation } from 'vs/workbench/api/common/extHost.protocol'; import { URI } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -36,9 +36,9 @@ class ExtHostEditorTab { } get apiObject(): vscode.Tab { - // Don't want to lose reference to parent `this` in the getters - const that = this; if (!this._apiObject) { + // Don't want to lose reference to parent `this` in the getters + const that = this; const obj: vscode.Tab = { get isActive() { // We use a getter function here to always ensure at most 1 active tab per group and prevent iteration for being required @@ -120,9 +120,9 @@ class ExtHostEditorTabGroup { } get apiObject(): vscode.TabGroup { - // Don't want to lose reference to parent `this` in the getters - const that = this; if (!this._apiObject) { + // Don't want to lose reference to parent `this` in the getters + const that = this; const obj: vscode.TabGroup = { get isActive() { // We use a getter function here to always ensure at most 1 active group and prevent iteration for being required @@ -155,15 +155,39 @@ class ExtHostEditorTabGroup { this._dto = dto; } - acceptTabDtoUpdate(dto: IEditorTabDto) { - const tab = this._tabs.find(extHostTab => extHostTab.tabId === dto.id); + acceptTabOperation(operation: TabOperation): ExtHostEditorTab { + // In the open case we add the tab to the group + if (operation.kind === TabModelOperationKind.TAB_OPEN) { + const tab = new ExtHostEditorTab(operation.tabDto, this, () => this.activeTabId()); + // Insert tab at editor index + this._tabs.splice(operation.index, 0, tab); + if (operation.tabDto.isActive) { + this._activeTabId = tab.tabId; + } + return tab; + } else if (operation.kind === TabModelOperationKind.TAB_CLOSE) { + const tab = this._tabs.splice(operation.index, 1)[0]; + if (!tab) { + throw new Error(`Tab close updated received for index ${operation.index} which does not exist`); + } + if (tab.tabId === this._activeTabId) { + this._activeTabId = ''; + } + return tab; + } + const tab = this._tabs.find(extHostTab => extHostTab.tabId === operation.tabDto.id); if (!tab) { throw new Error('INVALID tab'); } - if (dto.isActive) { - this._activeTabId = dto.id; + if (operation.tabDto.isActive) { + this._activeTabId = operation.tabDto.id; + } else if (this._activeTabId === operation.tabDto.id && !operation.tabDto.isActive) { + // Events aren't guaranteed to be in order so if we receive a dto that matches the active tab id + // but isn't active we mark the active tab id as empty. This prevent onDidActiveTabChange frorm + // firing incorrectly + this._activeTabId = ''; } - tab.acceptDtoUpdate(dto); + tab.acceptDtoUpdate(operation.tabDto); return tab; } @@ -177,9 +201,8 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { readonly _serviceBrand: undefined; private readonly _proxy: MainThreadEditorTabsShape; - private readonly _onDidChangeTab = new Emitter<vscode.Tab>(); - private readonly _onDidChangeTabGroup = new Emitter<void>(); - private readonly _onDidChangeActiveTabGroup = new Emitter<vscode.TabGroup>(); + private readonly _onDidChangeTabs = new Emitter<vscode.Tab[]>(); + private readonly _onDidChangeTabGroups = new Emitter<vscode.TabGroup[]>(); // Have to use ! because this gets initialized via an RPC proxy private _activeGroupId!: number; @@ -197,9 +220,8 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { const that = this; const obj: vscode.TabGroups = { // never changes -> simple value - onDidChangeTabGroup: that._onDidChangeTabGroup.event, - onDidChangeActiveTabGroup: that._onDidChangeActiveTabGroup.event, - onDidChangeTab: that._onDidChangeTab.event, + onDidChangeTabGroups: that._onDidChangeTabGroups.event, + onDidChangeTabs: that._onDidChangeTabs.event, // dynamic -> getters get groups() { return Object.freeze(that._extHostTabGroups.map(group => group.apiObject)); @@ -219,8 +241,7 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { } extHostTabIds.push(extHostTab.tabId); } - this._proxy.$closeTab(extHostTabIds, preserveFocus); - return; + return this._proxy.$closeTab(extHostTabIds, preserveFocus); }, move: async (tab: vscode.Tab, viewColumn: ViewColumn, index: number, preservceFocus?: boolean) => { const extHostTab = this._findExtHostTabFromApi(tab); @@ -258,9 +279,8 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { const activeTabGroupId = assertIsDefined(tabGroups.find(group => group.isActive === true)?.groupId); if (activeTabGroupId !== undefined && this._activeGroupId !== activeTabGroupId) { this._activeGroupId = activeTabGroupId; - this._onDidChangeActiveTabGroup.fire(this.tabGroups.activeTabGroup); } - this._onDidChangeTabGroup.fire(); + this._onDidChangeTabGroups.fire(this._extHostTabGroups.map(g => g.apiObject)); } $acceptTabGroupUpdate(groupDto: IEditorTabGroupDto) { @@ -270,86 +290,20 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { } group.acceptGroupDtoUpdate(groupDto); if (groupDto.isActive) { - const oldActiveGroupId = this._activeGroupId; this._activeGroupId = groupDto.groupId; - if (oldActiveGroupId !== this._activeGroupId) { - this._onDidChangeActiveTabGroup.fire(group.apiObject); - } } - this._onDidChangeTabGroup.fire(); + this._onDidChangeTabGroups.fire([group.apiObject]); } - $acceptTabUpdate(groupId: number, tabDto: IEditorTabDto) { - const group = this._extHostTabGroups.find(group => group.groupId === groupId); + $acceptTabOperation(operation: TabOperation) { + const group = this._extHostTabGroups.find(group => group.groupId === operation.groupId); if (!group) { throw new Error('Update Tabs IPC call received before group creation.'); } - const tab = group.acceptTabDtoUpdate(tabDto); - this._onDidChangeTab.fire(tab.apiObject); + const tab = group.acceptTabOperation(operation); + // We don't want to fire a change event with a closed tab to prevent an invalid tabs from being received + if (operation.kind !== TabModelOperationKind.TAB_CLOSE) { + this._onDidChangeTabs.fire([tab.apiObject]); + } } - - /** - * Compares two groups determining if they're the same or different - * @param group1 The first group to compare - * @param group2 The second group to compare - * @returns True if different, false otherwise - */ - // private groupDiff(group1: IEditorTabGroup | undefined, group2: IEditorTabGroup | undefined): boolean { - // if (group1 === group2) { - // return false; - // } - // // They would be reference equal if both undefined so one is undefined and one isn't hence different - // if (!group1 || !group2) { - // return true; - // } - // if (group1.isActive !== group2.isActive - // || group1.viewColumn !== group2.viewColumn - // || group1.tabs.length !== group2.tabs.length - // ) { - // return true; - // } - // for (let i = 0; i < group1.tabs.length; i++) { - // if (this.tabDiff(group1.tabs[i], group2.tabs[i])) { - // return true; - // } - // } - // return false; - // } - - /** - * Compares two tabs determining if they're the same or different - * @param tab1 The first tab to compare - * @param tab2 The second tab to compare - * @returns True if different, false otherwise - */ - // private tabDiff(tab1: IEditorTab | undefined, tab2: IEditorTab | undefined): boolean { - // if (tab1 === tab2) { - // return false; - // } - // // They would be reference equal if both undefined so one is undefined and one isn't therefore they're different - // if (!tab1 || !tab2) { - // return true; - // } - // if (tab1.label !== tab2.label - // || tab1.viewColumn !== tab2.viewColumn - // || tab1.resource?.toString() !== tab2.resource?.toString() - // || tab1.viewType !== tab2.viewType - // || tab1.isActive !== tab2.isActive - // || tab1.isPinned !== tab2.isPinned - // || tab1.isDirty !== tab2.isDirty - // || tab1.additionalResourcesAndViewTypes.length !== tab2.additionalResourcesAndViewTypes.length - // ) { - // return true; - // } - // for (let i = 0; i < tab1.additionalResourcesAndViewTypes.length; i++) { - // const tab1Resource = tab1.additionalResourcesAndViewTypes[i].resource; - // const tab2Resource = tab2.additionalResourcesAndViewTypes[i].resource; - // const tab1viewType = tab1.additionalResourcesAndViewTypes[i].viewType; - // const tab2viewType = tab2.additionalResourcesAndViewTypes[i].viewType; - // if (tab1Resource?.toString() !== tab2Resource?.toString() || tab1viewType !== tab2viewType) { - // return true; - // } - // } - // return false; - // } } diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 3042ed465c3..b3ff5c0c2b4 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -611,7 +611,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } // Require the test runner via node require from the provided path - const testRunner: ITestRunner | INewTestRunner | undefined = await this._loadCommonJSModule(null, extensionTestsLocationURI, new ExtensionActivationTimesBuilder(false)); + const testRunner = await this._loadCommonJSModule<ITestRunner | INewTestRunner | undefined>(null, extensionTestsLocationURI, new ExtensionActivationTimesBuilder(false)); if (!testRunner || typeof testRunner.run !== 'function') { throw new Error(nls.localize('extensionTestError', "Path {0} does not point to a valid extension test runner.", extensionTestsLocationURI.toString())); @@ -848,7 +848,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme protected abstract _beforeAlmostReadyToRunExtensions(): Promise<void>; protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined; - protected abstract _loadCommonJSModule<T>(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T>; + protected abstract _loadCommonJSModule<T extends object | undefined>(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T>; public abstract $setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void>; } @@ -897,7 +897,7 @@ export interface IExtHostExtensionService extends AbstractExtHostExtensionServic getRemoteConnectionData(): IRemoteConnectionData | null; } -export class Extension<T> implements vscode.Extension<T> { +export class Extension<T extends object | null | undefined> implements vscode.Extension<T> { #extensionService: IExtHostExtensionService; #originExtensionId: ExtensionIdentifier; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index e5ca7469fb0..2cc57319c69 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -35,6 +35,8 @@ import { isCancellationError } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { raceCancellationError } from 'vs/base/common/async'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { DataTransferConverter, DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer'; +import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; // --- adapter @@ -1712,6 +1714,27 @@ class TypeHierarchyAdapter { return map?.get(itemId); } } + +class DocumentOnDropAdapter { + + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.DocumentOnDropProvider + ) { } + + async provideDocumentOnDropEdits(uri: URI, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise<Dto<languages.SnippetTextEdit> | undefined> { + const doc = this._documents.getDocument(uri); + const pos = typeConvert.Position.to(position); + const dataTransfer = DataTransferConverter.toDataTransfer(dataTransferDto); + + const edit = await this._provider.provideDocumentOnDropEdits(doc, pos, dataTransfer, token); + if (!edit) { + return undefined; + } + return typeConvert.SnippetTextEdit.from(edit); + } +} + type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter @@ -1720,7 +1743,8 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | SelectionRangeAdapter | CallHierarchyAdapter | TypeHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter | InlineValuesAdapter - | LinkedEditingRangeAdapter | InlayHintsAdapter | InlineCompletionAdapter | InlineCompletionAdapterNew; + | LinkedEditingRangeAdapter | InlayHintsAdapter | InlineCompletionAdapter | InlineCompletionAdapterNew + | DocumentOnDropAdapter; class AdapterData { constructor( @@ -2341,6 +2365,18 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._withAdapter(handle, TypeHierarchyAdapter, adapter => Promise.resolve(adapter.releaseSession(sessionId)), undefined, undefined); } + // --- Document on drop + + registerDocumentOnDropProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentOnDropProvider) { + const handle = this._addNewAdapter(new DocumentOnDropAdapter(this._documents, provider), extension); + this._proxy.$registerDocumentOnDropProvider(handle, this._transformDocumentSelector(selector)); + return this._createDisposable(handle); + } + + $provideDocumentOnDropEdits(handle: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise<Dto<languages.SnippetTextEdit> | undefined> { + return this._withAdapter(handle, DocumentOnDropAdapter, adapter => Promise.resolve(adapter.provideDocumentOnDropEdits(URI.revive(resource), position, dataTransferDto, token)), undefined, undefined); + } + // --- configuration private static _serializeRegExp(regExp: RegExp): extHostProtocol.IRegExpDto { diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 465d2dc9e4d..6d68184d52c 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -17,7 +17,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { Cache } from 'vs/workbench/api/common/cache'; import { ExtHostNotebookShape, IMainContext, IModelAddedData, INotebookCellStatusBarListDto, INotebookDocumentsAndEditorsDelta, INotebookDocumentShowOptions, INotebookEditorAddData, MainContext, MainThreadNotebookDocumentsShape, MainThreadNotebookEditorsShape, MainThreadNotebookShape, NotebookDataDto } from 'vs/workbench/api/common/extHost.protocol'; -import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { ApiCommand, ApiCommandArgument, ApiCommandResult, CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; @@ -48,12 +48,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { private readonly _editors = new Map<string, ExtHostNotebookEditor>(); private readonly _commandsConverter: CommandsConverter; - private readonly _onDidChangeNotebookCells = new Emitter<vscode.NotebookCellsChangeEvent>(); - readonly onDidChangeNotebookCells = this._onDidChangeNotebookCells.event; - private readonly _onDidChangeCellOutputs = new Emitter<vscode.NotebookCellOutputsChangeEvent>(); - readonly onDidChangeCellOutputs = this._onDidChangeCellOutputs.event; - private readonly _onDidChangeCellMetadata = new Emitter<vscode.NotebookCellMetadataChangeEvent>(); - readonly onDidChangeCellMetadata = this._onDidChangeCellMetadata.event; private readonly _onDidChangeActiveNotebookEditor = new Emitter<vscode.NotebookEditor | undefined>(); readonly onDidChangeActiveNotebookEditor = this._onDidChangeActiveNotebookEditor.event; @@ -104,6 +98,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { return arg; } }); + + ExtHostNotebookController._registerApiCommands(commands); } getEditorById(editorId: string): ExtHostNotebookEditor { @@ -442,23 +438,11 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { if (this._documents.has(uri)) { throw new Error(`adding EXISTING notebook ${uri} `); } - const that = this; const document = new ExtHostNotebookDocument( this._notebookDocumentsProxy, this._textDocumentsAndEditors, this._textDocuments, - { - emitModelChange(event: vscode.NotebookCellsChangeEvent): void { - that._onDidChangeNotebookCells.fire(event); - }, - emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void { - that._onDidChangeCellOutputs.fire(event); - }, - emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void { - that._onDidChangeCellMetadata.fire(event); - } - }, uri, modelData ); @@ -531,4 +515,24 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { this._onDidChangeActiveNotebookEditor.fire(this._activeNotebookEditor?.apiEditor); } } + + private static _registerApiCommands(extHostCommands: ExtHostCommands) { + + const notebookTypeArg = ApiCommandArgument.String.with('notebookType', 'A notebook type'); + + const commandDataToNotebook = new ApiCommand( + 'vscode.executeDataToNotebook', '_executeDataToNotebook', 'Invoke notebook serializer', + [notebookTypeArg, new ApiCommandArgument<Uint8Array, VSBuffer>('data', 'Bytes to convert to data', v => v instanceof Uint8Array, v => VSBuffer.wrap(v))], + new ApiCommandResult<NotebookDataDto, vscode.NotebookData>('Notebook Data', dto => typeConverters.NotebookData.to(dto)) + ); + + const commandNotebookToData = new ApiCommand( + 'vscode.executeNotebookToData', '_executeNotebookToData', 'Invoke notebook serializer', + [notebookTypeArg, new ApiCommandArgument<vscode.NotebookData, NotebookDataDto>('NotebookData', 'Notebook data to convert to bytes', v => true, v => typeConverters.NotebookData.from(v))], + new ApiCommandResult<VSBuffer, Uint8Array>('Bytes', dto => dto.buffer) + ); + + extHostCommands.registerApiCommand(commandDataToNotebook); + extHostCommands.registerApiCommand(commandNotebookToData); + } } diff --git a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts index 21c082ba14a..65ecebc97c1 100644 --- a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts @@ -6,7 +6,6 @@ import * as types from 'vs/workbench/api/common/extHostTypes'; import * as vscode from 'vscode'; import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { PrefixSumComputer } from 'vs/editor/common/model/prefixSumComputer'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -14,6 +13,7 @@ import { score } from 'vs/editor/common/languageSelector'; import { ResourceMap } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; +import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextDocument { @@ -32,7 +32,7 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD readonly uri = URI.from({ scheme: 'vscode-concat-doc', path: generateUuid() }); constructor( - extHostNotebooks: ExtHostNotebookController, + extHostNotebooks: ExtHostNotebookDocuments, extHostDocuments: ExtHostDocuments, private readonly _notebook: vscode.NotebookDocument, private readonly _selector: vscode.DocumentSelector | undefined, @@ -56,7 +56,7 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD } }; - this._disposables.add(extHostNotebooks.onDidChangeNotebookCells(e => documentChange(e.document))); + this._disposables.add(extHostNotebooks.onDidChangeNotebookDocument(e => documentChange(e.notebook))); } dispose(): void { diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index b1094016a64..e8cc8e027ff 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { Schemas } from 'vs/base/common/network'; -import { deepFreeze, equals } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; @@ -16,10 +15,12 @@ import * as vscode from 'vscode'; class RawContentChangeEvent { - - constructor(readonly start: number, readonly deletedCount: number, readonly deletedItems: vscode.NotebookCell[], readonly items: ExtHostCell[]) { - - } + constructor( + readonly start: number, + readonly deletedCount: number, + readonly deletedItems: vscode.NotebookCell[], + readonly items: ExtHostCell[] + ) { } asApiEvent(): vscode.NotebookDocumentContentChange { return { @@ -28,18 +29,6 @@ class RawContentChangeEvent { removedCells: this.deletedItems, }; } - - - static asApiEvents(events: RawContentChangeEvent[]): readonly vscode.NotebookCellsChangeData[] { - return events.map(event => { - return { - start: event.start, - deletedCount: event.deletedCount, - deletedItems: event.deletedItems, - items: event.items.map(data => data.apiCell) - }; - }); - } } export class ExtHostCell { @@ -138,12 +127,6 @@ export class ExtHostCell { } } -export interface INotebookEventEmitter { - emitModelChange(events: vscode.NotebookCellsChangeEvent): void; - emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void; - emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void; -} - export class ExtHostNotebookDocument { @@ -165,7 +148,6 @@ export class ExtHostNotebookDocument { private readonly _proxy: extHostProtocol.MainThreadNotebookDocumentsShape, private readonly _textDocumentsAndEditors: ExtHostDocumentsAndEditors, private readonly _textDocuments: ExtHostDocuments, - private readonly _emitter: INotebookEventEmitter, readonly uri: URI, data: extHostProtocol.INotebookModelAddedData ) { @@ -260,7 +242,7 @@ export class ExtHostNotebookDocument { this._setCellOutputItems(rawEvent.index, rawEvent.outputId, rawEvent.append, rawEvent.outputItems); relaxedCellChanges.push({ cell: this._cells[rawEvent.index].apiCell, outputs: this._cells[rawEvent.index].apiCell.outputs }); - } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeLanguage) { + } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellLanguage) { this._changeCellLanguage(rawEvent.index, rawEvent.language); relaxedCellChanges.push({ cell: this._cells[rawEvent.index].apiCell, document: this._cells[rawEvent.index].apiCell.document }); @@ -388,13 +370,6 @@ export class ExtHostNotebookDocument { bucket.push(changeEvent.asApiEvent()); } } - - if (!initialization) { - this._emitter.emitModelChange(deepFreeze({ - document: this.apiNotebook, - changes: RawContentChangeEvent.asApiEvents(contentChangeEvents) - })); - } } private _moveCell(index: number, newIdx: number, bucket: vscode.NotebookDocumentContentChange[]): void { @@ -407,22 +382,16 @@ export class ExtHostNotebookDocument { for (const change of changes) { bucket.push(change.asApiEvent()); } - this._emitter.emitModelChange(deepFreeze({ - document: this.apiNotebook, - changes: RawContentChangeEvent.asApiEvents(changes) - })); } private _setCellOutputs(index: number, outputs: extHostProtocol.NotebookOutputDto[]): void { const cell = this._cells[index]; cell.setOutputs(outputs); - this._emitter.emitCellOutputsChange(deepFreeze({ document: this.apiNotebook, cells: [cell.apiCell] })); } private _setCellOutputItems(index: number, outputId: string, append: boolean, outputItems: extHostProtocol.NotebookOutputItemDto[]): void { const cell = this._cells[index]; cell.setOutputItems(outputId, append, outputItems); - this._emitter.emitCellOutputsChange(deepFreeze({ document: this.apiNotebook, cells: [cell.apiCell] })); } private _changeCellLanguage(index: number, newLanguageId: string): void { @@ -439,14 +408,7 @@ export class ExtHostNotebookDocument { private _changeCellMetadata(index: number, newMetadata: notebookCommon.NotebookCellMetadata): void { const cell = this._cells[index]; - - const originalExtMetadata = cell.apiCell.metadata; cell.setMetadata(newMetadata); - const newExtMetadata = cell.apiCell.metadata; - - if (!equals(originalExtMetadata, newExtMetadata)) { - this._emitter.emitCellMetadataChange(deepFreeze({ document: this.apiNotebook, cell: cell.apiCell })); - } } private _changeCellInternalMetadata(index: number, newInternalMetadata: notebookCommon.NotebookCellInternalMetadata): void { diff --git a/src/vs/workbench/api/common/extHostNotebookDocuments.ts b/src/vs/workbench/api/common/extHostNotebookDocuments.ts index fbb400ec64d..b89b9cd1f55 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocuments.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocuments.ts @@ -5,7 +5,6 @@ import { Emitter } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { ILogService } from 'vs/platform/log/common/log'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -14,9 +13,6 @@ import type * as vscode from 'vscode'; export class ExtHostNotebookDocuments implements extHostProtocol.ExtHostNotebookDocumentsShape { - private readonly _onDidChangeNotebookDocumentMetadata = new Emitter<vscode.NotebookDocumentMetadataChangeEvent>(); - readonly onDidChangeNotebookDocumentMetadata = this._onDidChangeNotebookDocumentMetadata.event; - private readonly _onDidSaveNotebookDocument = new Emitter<vscode.NotebookDocument>(); readonly onDidSaveNotebookDocument = this._onDidSaveNotebookDocument.event; @@ -24,7 +20,6 @@ export class ExtHostNotebookDocuments implements extHostProtocol.ExtHostNotebook readonly onDidChangeNotebookDocument = this._onDidChangeNotebookDocument.event; constructor( - @ILogService private readonly _logService: ILogService, private readonly _notebooksAndEditors: ExtHostNotebookController, ) { } @@ -43,13 +38,4 @@ export class ExtHostNotebookDocuments implements extHostProtocol.ExtHostNotebook const document = this._notebooksAndEditors.getNotebookDocument(URI.revive(uri)); this._onDidSaveNotebookDocument.fire(document.apiNotebook); } - - $acceptDocumentPropertiesChanged(uri: UriComponents, data: extHostProtocol.INotebookDocumentPropertiesChangeData): void { - this._logService.debug('ExtHostNotebook#$acceptDocumentPropertiesChanged', uri.path, data); - const document = this._notebooksAndEditors.getNotebookDocument(URI.revive(uri)); - document.acceptDocumentPropertiesChanged(data); - if (data.metadata) { - this._onDidChangeNotebookDocumentMetadata.fire({ document: document.apiNotebook }); - } - } } diff --git a/src/vs/workbench/api/common/extHostTestItem.ts b/src/vs/workbench/api/common/extHostTestItem.ts new file mode 100644 index 00000000000..35440b3b0fc --- /dev/null +++ b/src/vs/workbench/api/common/extHostTestItem.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as editorRange from 'vs/editor/common/core/range'; +import { createPrivateApiFor, getPrivateApiFor, IExtHostTestItemApi } from 'vs/workbench/api/common/extHostTestingPrivateApi'; +import { TestId, TestIdPathParts } from 'vs/workbench/contrib/testing/common/testId'; +import { createTestItemChildren, ExtHostTestItemEvent, ITestChildrenLike, ITestItemApi, ITestItemChildren, TestItemCollection, TestItemEventOp } from 'vs/workbench/contrib/testing/common/testItemCollection'; +import { denamespaceTestTag, ITestItem, ITestItemContext } from 'vs/workbench/contrib/testing/common/testTypes'; +import type * as vscode from 'vscode'; +import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; +import { URI } from 'vs/base/common/uri'; + +const testItemPropAccessor = <K extends keyof vscode.TestItem>( + api: IExtHostTestItemApi, + defaultValue: vscode.TestItem[K], + equals: (a: vscode.TestItem[K], b: vscode.TestItem[K]) => boolean, + toUpdate: (newValue: vscode.TestItem[K], oldValue: vscode.TestItem[K]) => ExtHostTestItemEvent, +) => { + let value = defaultValue; + return { + enumerable: true, + configurable: false, + get() { + return value; + }, + set(newValue: vscode.TestItem[K]) { + if (!equals(value, newValue)) { + const oldValue = value; + value = newValue; + api.listener?.(toUpdate(newValue, oldValue)); + } + }, + }; +}; + +type WritableProps = Pick<vscode.TestItem, 'range' | 'label' | 'description' | 'sortText' | 'canResolveChildren' | 'busy' | 'error' | 'tags'>; + +const strictEqualComparator = <T>(a: T, b: T) => a === b; + +const propComparators: { [K in keyof Required<WritableProps>]: (a: vscode.TestItem[K], b: vscode.TestItem[K]) => boolean } = { + range: (a, b) => { + if (a === b) { return true; } + if (!a || !b) { return false; } + return a.isEqual(b); + }, + label: strictEqualComparator, + description: strictEqualComparator, + sortText: strictEqualComparator, + busy: strictEqualComparator, + error: strictEqualComparator, + canResolveChildren: strictEqualComparator, + tags: (a, b) => { + if (a.length !== b.length) { + return false; + } + + if (a.some(t1 => !b.find(t2 => t1.id === t2.id))) { + return false; + } + + return true; + }, +}; + +const evSetProps = <T>(fn: (newValue: T) => Partial<ITestItem>): (newValue: T) => ExtHostTestItemEvent => + v => ({ op: TestItemEventOp.SetProp, update: fn(v) }); + +const makePropDescriptors = (api: IExtHostTestItemApi, label: string): { [K in keyof Required<WritableProps>]: PropertyDescriptor } => ({ + range: testItemPropAccessor<'range'>(api, undefined, propComparators.range, evSetProps(r => ({ range: editorRange.Range.lift(Convert.Range.from(r)) }))), + label: testItemPropAccessor<'label'>(api, label, propComparators.label, evSetProps(label => ({ label }))), + description: testItemPropAccessor<'description'>(api, undefined, propComparators.description, evSetProps(description => ({ description }))), + sortText: testItemPropAccessor<'sortText'>(api, undefined, propComparators.sortText, evSetProps(sortText => ({ sortText }))), + canResolveChildren: testItemPropAccessor<'canResolveChildren'>(api, false, propComparators.canResolveChildren, state => ({ + op: TestItemEventOp.UpdateCanResolveChildren, + state, + })), + busy: testItemPropAccessor<'busy'>(api, false, propComparators.busy, evSetProps(busy => ({ busy }))), + error: testItemPropAccessor<'error'>(api, undefined, propComparators.error, evSetProps(error => ({ error: Convert.MarkdownString.fromStrict(error) || null }))), + tags: testItemPropAccessor<'tags'>(api, [], propComparators.tags, (current, previous) => ({ + op: TestItemEventOp.SetTags, + new: current.map(Convert.TestTag.from), + old: previous.map(Convert.TestTag.from), + })), +}); + +const toItemFromPlain = (item: ITestItem.Serialized): TestItemImpl => { + const testId = TestId.fromString(item.extId); + const testItem = new TestItemImpl(testId.controllerId, testId.localId, item.label, URI.revive(item.uri) || undefined); + testItem.range = Convert.Range.to(item.range || undefined); + testItem.description = item.description || undefined; + testItem.sortText = item.sortText || undefined; + testItem.tags = item.tags.map(t => Convert.TestTag.to({ id: denamespaceTestTag(t).tagId })); + return testItem; +}; + +export const toItemFromContext = (context: ITestItemContext): TestItemImpl => { + let node: TestItemImpl | undefined; + for (const test of context.tests) { + const next = toItemFromPlain(test.item); + getPrivateApiFor(next).parent = node; + node = next; + } + + return node!; +}; + +export class TestItemImpl implements vscode.TestItem { + public readonly id!: string; + public readonly uri!: vscode.Uri | undefined; + public readonly children!: ITestItemChildren<vscode.TestItem>; + public readonly parent!: TestItemImpl | undefined; + + public range!: vscode.Range | undefined; + public description!: string | undefined; + public sortText!: string | undefined; + public label!: string; + public error!: string | vscode.MarkdownString; + public busy!: boolean; + public canResolveChildren!: boolean; + public tags!: readonly vscode.TestTag[]; + + /** + * Note that data is deprecated and here for back-compat only + */ + constructor(controllerId: string, id: string, label: string, uri: vscode.Uri | undefined) { + if (id.includes(TestIdPathParts.Delimiter)) { + throw new Error(`Test IDs may not include the ${JSON.stringify(id)} symbol`); + } + + const api = createPrivateApiFor(this, controllerId); + Object.defineProperties(this, { + id: { + value: id, + enumerable: true, + writable: false, + }, + uri: { + value: uri, + enumerable: true, + writable: false, + }, + parent: { + enumerable: false, + get() { + return api.parent instanceof TestItemRootImpl ? undefined : api.parent; + }, + }, + children: { + value: createTestItemChildren(api, getPrivateApiFor, TestItemImpl), + enumerable: true, + writable: false, + }, + ...makePropDescriptors(api, label), + }); + } +} + +export class TestItemRootImpl extends TestItemImpl { + constructor(controllerId: string, label: string) { + super(controllerId, controllerId, label, undefined); + } +} + +export class ExtHostTestItemCollection extends TestItemCollection<TestItemImpl> { + constructor(controllerId: string, controllerLabel: string) { + super({ + controllerId, + getApiFor: getPrivateApiFor as (impl: TestItemImpl) => ITestItemApi<TestItemImpl>, + getChildren: (item) => item.children as ITestChildrenLike<TestItemImpl>, + root: new TestItemRootImpl(controllerId, controllerLabel), + toITestItem: Convert.TestItem.from, + }); + } +} diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 70ea3824623..a7c81eb67d2 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -17,18 +17,18 @@ import { generateUuid } from 'vs/base/common/uuid'; import { ExtHostTestingShape, ILocationDto, MainContext, MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { InvalidTestItemError, TestItemImpl, TestItemRootImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; +import { ExtHostTestItemCollection, TestItemImpl, TestItemRootImpl, toItemFromContext } from 'vs/workbench/api/common/extHostTestItem'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; import { TestRunProfileKind, TestRunRequest } from 'vs/workbench/api/common/extHostTypes'; -import { SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; -import { AbstractIncrementalTestCollection, CoverageDetails, IFileCoverage, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestItem, RunTestForControllerRequest, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; +import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; +import { AbstractIncrementalTestCollection, CoverageDetails, IFileCoverage, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestItem, RunTestForControllerRequest, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import type * as vscode from 'vscode'; interface ControllerInfo { controller: vscode.TestController; profiles: Map<number, vscode.TestRunProfile>; - collection: SingleUseTestCollection; + collection: ExtHostTestItemCollection; } export class ExtHostTesting implements ExtHostTestingShape { @@ -48,7 +48,7 @@ export class ExtHostTesting implements ExtHostTestingShape { commands.registerArgumentProcessor({ processArgument: arg => - arg?.$mid === MarshalledId.TestItemContext ? Convert.TestItem.toItemFromContext(arg) : arg, + arg?.$mid === MarshalledId.TestItemContext ? toItemFromContext(arg) : arg, }); } @@ -61,7 +61,7 @@ export class ExtHostTesting implements ExtHostTestingShape { } const disposable = new DisposableStore(); - const collection = disposable.add(new SingleUseTestCollection(controllerId)); + const collection = disposable.add(new ExtHostTestItemCollection(controllerId, label)); collection.root.label = label; const profiles = new Map<number, vscode.TestRunProfile>(); @@ -107,7 +107,7 @@ export class ExtHostTesting implements ExtHostTestingShape { collection.resolveHandler = fn; }, get resolveHandler() { - return collection.resolveHandler; + return collection.resolveHandler as undefined | ((item?: vscode.TestItem) => void); }, dispose: () => { disposable.dispose(); @@ -527,7 +527,7 @@ export class TestRunCoordinator { /** * Implements the public `createTestRun` API. */ - public createTestRun(controllerId: string, collection: SingleUseTestCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { + public createTestRun(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { const existing = this.tracked.get(request); if (existing) { return existing.createRun(name); @@ -575,7 +575,7 @@ export class TestRunDto { private readonly includePrefix: string[]; private readonly excludePrefix: string[]; - public static fromPublic(controllerId: string, collection: SingleUseTestCollection, request: vscode.TestRunRequest, persist: boolean) { + public static fromPublic(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, persist: boolean) { return new TestRunDto( controllerId, generateUuid(), @@ -586,7 +586,7 @@ export class TestRunDto { ); } - public static fromInternal(request: RunTestForControllerRequest, collection: SingleUseTestCollection) { + public static fromInternal(request: RunTestForControllerRequest, collection: ExtHostTestItemCollection) { return new TestRunDto( request.controllerId, request.runId, @@ -603,7 +603,7 @@ export class TestRunDto { include: string[], exclude: string[], public readonly isPersisted: boolean, - public readonly colllection: SingleUseTestCollection, + public readonly colllection: ExtHostTestItemCollection, ) { this.includePrefix = include.map(id => id + TestIdPathParts.Delimiter); this.excludePrefix = exclude.map(id => id + TestIdPathParts.Delimiter); @@ -966,3 +966,4 @@ const profileGroupToBitset: { [K in TestRunProfileKind]: TestRunProfileBitset } [TestRunProfileKind.Debug]: TestRunProfileBitset.Debug, [TestRunProfileKind.Run]: TestRunProfileBitset.Run, }; + diff --git a/src/vs/workbench/api/common/extHostTestingPrivateApi.ts b/src/vs/workbench/api/common/extHostTestingPrivateApi.ts index 26c7081cce3..f0870fd5a00 100644 --- a/src/vs/workbench/api/common/extHostTestingPrivateApi.ts +++ b/src/vs/workbench/api/common/extHostTestingPrivateApi.ts @@ -3,58 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TestIdPathParts } from 'vs/workbench/contrib/testing/common/testId'; +import { ExtHostTestItemEvent, InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; import * as vscode from 'vscode'; -export const enum ExtHostTestItemEventOp { - Upsert, - RemoveChild, - Invalidated, - SetProp, - Bulk, -} - -export interface ITestItemUpsertChild { - op: ExtHostTestItemEventOp.Upsert; - item: TestItemImpl; -} - -export interface ITestItemRemoveChild { - op: ExtHostTestItemEventOp.RemoveChild; - id: string; -} - -export interface ITestItemInvalidated { - op: ExtHostTestItemEventOp.Invalidated; -} - -export interface ITestItemSetProp { - op: ExtHostTestItemEventOp.SetProp; - key: keyof vscode.TestItem; - value: any; - previous: any; -} -export interface ITestItemBulkReplace { - op: ExtHostTestItemEventOp.Bulk; - ops: (ITestItemUpsertChild | ITestItemRemoveChild)[]; -} - -export type ExtHostTestItemEvent = - | ITestItemUpsertChild - | ITestItemRemoveChild - | ITestItemInvalidated - | ITestItemSetProp - | ITestItemBulkReplace; - export interface IExtHostTestItemApi { controllerId: string; - parent?: TestItemImpl; + parent?: vscode.TestItem; listener?: (evt: ExtHostTestItemEvent) => void; } -const eventPrivateApis = new WeakMap<TestItemImpl, IExtHostTestItemApi>(); +const eventPrivateApis = new WeakMap<vscode.TestItem, IExtHostTestItemApi>(); -export const createPrivateApiFor = (impl: TestItemImpl, controllerId: string) => { +export const createPrivateApiFor = (impl: vscode.TestItem, controllerId: string) => { const api: IExtHostTestItemApi = { controllerId }; eventPrivateApis.set(impl, api); return api; @@ -65,261 +25,11 @@ export const createPrivateApiFor = (impl: TestItemImpl, controllerId: string) => * is a managed object, but we keep a weakmap to avoid exposing any of the * internals to extensions. */ -export const getPrivateApiFor = (impl: TestItemImpl) => eventPrivateApis.get(impl)!; - -const testItemPropAccessor = <K extends keyof vscode.TestItem>( - api: IExtHostTestItemApi, - key: K, - defaultValue: vscode.TestItem[K], - equals: (a: vscode.TestItem[K], b: vscode.TestItem[K]) => boolean -) => { - let value = defaultValue; - return { - enumerable: true, - configurable: false, - get() { - return value; - }, - set(newValue: vscode.TestItem[K]) { - if (!equals(value, newValue)) { - const oldValue = value; - value = newValue; - api.listener?.({ - op: ExtHostTestItemEventOp.SetProp, - key, - value: newValue, - previous: oldValue, - }); - } - }, - }; -}; - -type WritableProps = Pick<vscode.TestItem, 'range' | 'label' | 'description' | 'sortText' | 'canResolveChildren' | 'busy' | 'error' | 'tags'>; - -const strictEqualComparator = <T>(a: T, b: T) => a === b; - -const propComparators: { [K in keyof Required<WritableProps>]: (a: vscode.TestItem[K], b: vscode.TestItem[K]) => boolean } = { - range: (a, b) => { - if (a === b) { return true; } - if (!a || !b) { return false; } - return a.isEqual(b); - }, - label: strictEqualComparator, - description: strictEqualComparator, - sortText: strictEqualComparator, - busy: strictEqualComparator, - error: strictEqualComparator, - canResolveChildren: strictEqualComparator, - tags: (a, b) => { - if (a.length !== b.length) { - return false; - } - - if (a.some(t1 => !b.find(t2 => t1.id === t2.id))) { - return false; - } - - return true; - }, -}; - -const writablePropKeys = Object.keys(propComparators) as (keyof Required<WritableProps>)[]; - -const makePropDescriptors = (api: IExtHostTestItemApi, label: string): { [K in keyof Required<WritableProps>]: PropertyDescriptor } => ({ - range: testItemPropAccessor(api, 'range', undefined, propComparators.range), - label: testItemPropAccessor(api, 'label', label, propComparators.label), - description: testItemPropAccessor(api, 'description', undefined, propComparators.description), - sortText: testItemPropAccessor(api, 'sortText', undefined, propComparators.sortText), - canResolveChildren: testItemPropAccessor(api, 'canResolveChildren', false, propComparators.canResolveChildren), - busy: testItemPropAccessor(api, 'busy', false, propComparators.busy), - error: testItemPropAccessor(api, 'error', undefined, propComparators.error), - tags: testItemPropAccessor(api, 'tags', [], propComparators.tags), -}); - -/** - * Returns a partial test item containing the writable properties in B that - * are different from A. - */ -export const diffTestItems = (a: vscode.TestItem, b: vscode.TestItem) => { - const output = new Map<keyof WritableProps, unknown>(); - for (const key of writablePropKeys) { - const cmp = propComparators[key] as (a: unknown, b: unknown) => boolean; - if (!cmp(a[key], b[key])) { - output.set(key, b[key]); - } +export const getPrivateApiFor = (impl: vscode.TestItem) => { + const api = eventPrivateApis.get(impl); + if (!api) { + throw new InvalidTestItemError(impl?.id || '<unknown>'); } - return output; -}; - -export class DuplicateTestItemError extends Error { - constructor(id: string) { - super(`Attempted to insert a duplicate test item ID ${id}`); - } -} - -export class InvalidTestItemError extends Error { - constructor(id: string) { - super(`TestItem with ID "${id}" is invalid. Make sure to create it from the createTestItem method.`); - } -} - -export class MixedTestItemController extends Error { - constructor(id: string, ctrlA: string, ctrlB: string) { - super(`TestItem with ID "${id}" is from controller "${ctrlA}" and cannot be added as a child of an item from controller "${ctrlB}".`); - } -} - - -export type TestItemCollectionImpl = vscode.TestItemCollection & { toJSON(): readonly TestItemImpl[] } & Iterable<TestItemImpl>; - -const createTestItemCollection = (owningItem: TestItemImpl): TestItemCollectionImpl => { - const api = getPrivateApiFor(owningItem); - let mapped = new Map<string, TestItemImpl>(); - - return { - /** @inheritdoc */ - get size() { - return mapped.size; - }, - - /** @inheritdoc */ - forEach(callback: (item: vscode.TestItem, collection: vscode.TestItemCollection) => unknown, thisArg?: unknown) { - for (const item of mapped.values()) { - callback.call(thisArg, item, this); - } - }, - - /** @inheritdoc */ - replace(items: Iterable<vscode.TestItem>) { - const newMapped = new Map<string, TestItemImpl>(); - const toDelete = new Set(mapped.keys()); - const bulk: ITestItemBulkReplace = { op: ExtHostTestItemEventOp.Bulk, ops: [] }; - - for (const item of items) { - if (!(item instanceof TestItemImpl)) { - throw new InvalidTestItemError(item.id); - } - - const itemController = getPrivateApiFor(item).controllerId; - if (itemController !== api.controllerId) { - throw new MixedTestItemController(item.id, itemController, api.controllerId); - } - - if (newMapped.has(item.id)) { - throw new DuplicateTestItemError(item.id); - } - - newMapped.set(item.id, item); - toDelete.delete(item.id); - bulk.ops.push({ op: ExtHostTestItemEventOp.Upsert, item }); - } - - for (const id of toDelete.keys()) { - bulk.ops.push({ op: ExtHostTestItemEventOp.RemoveChild, id }); - } - - api.listener?.(bulk); - - // important mutations come after firing, so if an error happens no - // changes will be "saved": - mapped = newMapped; - }, - - - /** @inheritdoc */ - add(item: vscode.TestItem) { - if (!(item instanceof TestItemImpl)) { - throw new InvalidTestItemError(item.id); - } - - mapped.set(item.id, item); - api.listener?.({ op: ExtHostTestItemEventOp.Upsert, item }); - }, - - /** @inheritdoc */ - delete(id: string) { - if (mapped.delete(id)) { - api.listener?.({ op: ExtHostTestItemEventOp.RemoveChild, id }); - } - }, - - /** @inheritdoc */ - get(itemId: string) { - return mapped.get(itemId); - }, - - /** JSON serialization function. */ - toJSON() { - return Array.from(mapped.values()); - }, - - /** @inheritdoc */ - [Symbol.iterator]() { - return mapped.values(); - }, - }; + return api; }; - -export class TestItemImpl implements vscode.TestItem { - public readonly id!: string; - public readonly uri!: vscode.Uri | undefined; - public readonly children!: TestItemCollectionImpl; - public readonly parent!: TestItemImpl | undefined; - - public range!: vscode.Range | undefined; - public description!: string | undefined; - public sortText!: string | undefined; - public label!: string; - public error!: string | vscode.MarkdownString; - public busy!: boolean; - public canResolveChildren!: boolean; - public tags!: readonly vscode.TestTag[]; - - /** - * Note that data is deprecated and here for back-compat only - */ - constructor(controllerId: string, id: string, label: string, uri: vscode.Uri | undefined) { - if (id.includes(TestIdPathParts.Delimiter)) { - throw new Error(`Test IDs may not include the ${JSON.stringify(id)} symbol`); - } - - const api = createPrivateApiFor(this, controllerId); - Object.defineProperties(this, { - id: { - value: id, - enumerable: true, - writable: false, - }, - uri: { - value: uri, - enumerable: true, - writable: false, - }, - parent: { - enumerable: false, - get() { - return api.parent instanceof TestItemRootImpl ? undefined : api.parent; - }, - }, - children: { - value: createTestItemCollection(this), - enumerable: true, - writable: false, - }, - ...makePropDescriptors(api, label), - }); - } - - /** @deprecated back compat */ - public invalidateResults() { - getPrivateApiFor(this).listener?.({ op: ExtHostTestItemEventOp.Invalidated }); - } -} - -export class TestItemRootImpl extends TestItemImpl { - constructor(controllerId: string, label: string) { - super(controllerId, controllerId, label, undefined); - } -} diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index db69a6a67bc..b221ea9710c 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -3,18 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AsyncEmitter, Emitter, Event } from 'vs/base/common/event'; import * as arrays from 'vs/base/common/arrays'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { ExtHostTextEditor, TextEditorDecorationType } from 'vs/workbench/api/common/extHostTextEditor'; import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { TextEditorSelectionChangeKind } from 'vs/workbench/api/common/extHostTypes'; import * as vscode from 'vscode'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { DataTransferConverter, DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer'; -import { IPosition } from 'vs/editor/common/core/position'; -import { CancellationToken } from 'vs/base/common/cancellation'; export class ExtHostEditors implements ExtHostEditorsShape { @@ -24,7 +21,6 @@ export class ExtHostEditors implements ExtHostEditorsShape { private readonly _onDidChangeTextEditorViewColumn = new Emitter<vscode.TextEditorViewColumnChangeEvent>(); private readonly _onDidChangeActiveTextEditor = new Emitter<vscode.TextEditor | undefined>(); private readonly _onDidChangeVisibleTextEditors = new Emitter<vscode.TextEditor[]>(); - private readonly _onWillDropOnTextEditor = new AsyncEmitter<vscode.TextEditorDropEvent>(); readonly onDidChangeTextEditorSelection: Event<vscode.TextEditorSelectionChangeEvent> = this._onDidChangeTextEditorSelection.event; readonly onDidChangeTextEditorOptions: Event<vscode.TextEditorOptionsChangeEvent> = this._onDidChangeTextEditorOptions.event; @@ -32,7 +28,6 @@ export class ExtHostEditors implements ExtHostEditorsShape { readonly onDidChangeTextEditorViewColumn: Event<vscode.TextEditorViewColumnChangeEvent> = this._onDidChangeTextEditorViewColumn.event; readonly onDidChangeActiveTextEditor: Event<vscode.TextEditor | undefined> = this._onDidChangeActiveTextEditor.event; readonly onDidChangeVisibleTextEditors: Event<vscode.TextEditor[]> = this._onDidChangeVisibleTextEditors.event; - readonly onWillDropOnTextEditor: Event<vscode.TextEditorDropEvent> = this._onWillDropOnTextEditor.event; private readonly _proxy: MainThreadTextEditorsShape; @@ -164,24 +159,4 @@ export class ExtHostEditors implements ExtHostEditorsShape { getDiffInformation(id: string): Promise<vscode.LineChange[]> { return Promise.resolve(this._proxy.$getDiffInformation(id)); } - - // --- Text editor drag and drop - - async $textEditorHandleDrop(id: string, position: IPosition, dataTransferDto: DataTransferDTO): Promise<void> { - const textEditor = this._extHostDocumentsAndEditors.getEditor(id); - if (!textEditor) { - throw new Error('Unknown text editor'); - } - - const pos = TypeConverters.Position.to(position); - const dataTransfer = DataTransferConverter.toDataTransfer(dataTransferDto); - - const event = Object.freeze({ - editor: textEditor.value, - position: pos, - dataTransfer: dataTransfer - }); - - await this._onWillDropOnTextEditor.fireAsync(event, CancellationToken.None); - } } diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index dbeca3fdaf4..123d24e4d46 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -18,12 +18,13 @@ import { isUndefinedOrNull, isString } from 'vs/base/common/types'; import { equals, coalesce } from 'vs/base/common/arrays'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { MarkdownString } from 'vs/workbench/api/common/extHostTypeConverters'; +import { MarkdownString, ViewBadge } from 'vs/workbench/api/common/extHostTypeConverters'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Command } from 'vs/editor/common/languages'; import { DataTransferConverter, DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer'; import { ITreeViewsService, TreeviewsService } from 'vs/workbench/services/views/common/treeViewsService'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { IDataTransfer } from 'vs/workbench/common/dnd'; type TreeItemHandle = string; @@ -115,6 +116,14 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { set description(description: string | undefined) { treeView.description = description; }, + get badge() { + checkProposedApiEnabled(extension, 'badges'); + return treeView.badge; + }, + set badge(badge: vscode.ViewBadge | undefined) { + checkProposedApiEnabled(extension, 'badges'); + treeView.badge = badge; + }, reveal: (element: T, options?: IRevealOptions): Promise<void> => { return treeView.reveal(element, options); }, @@ -407,6 +416,21 @@ class ExtHostTreeView<T> extends Disposable { this.proxy.$setTitle(this.viewId, this._title, description); } + private _badge: vscode.ViewBadge | undefined; + get badge(): vscode.ViewBadge | undefined { + return this._badge; + } + + set badge(badge: vscode.ViewBadge | undefined) { + if (this._badge?.value === badge?.value && + this._badge?.tooltip === badge?.tooltip) { + return; + } + + this._badge = ViewBadge.from(badge); + this.proxy.$setBadge(this.viewId, badge); + } + setExpanded(treeItemHandle: TreeItemHandle, expanded: boolean): void { const element = this.getExtensionElement(treeItemHandle); if (element) { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 28586947531..9dec8f6b163 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -10,7 +10,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; import { parse } from 'vs/base/common/marshalling'; import { cloneAndChange } from 'vs/base/common/objects'; -import { isDefined, isEmptyObject, isNumber, isString, withNullAsUndefined } from 'vs/base/common/types'; +import { isDefined, isEmptyObject, isNumber, isString, isUndefinedOrNull, withNullAsUndefined } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IURITransformer } from 'vs/base/common/uriIpc'; import { RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; @@ -25,13 +25,14 @@ import { EditorResolution, ITextEditorOptions } from 'vs/platform/editor/common/ import { IMarkerData, IRelatedInformation, MarkerSeverity, MarkerTag } from 'vs/platform/markers/common/markers'; import { ProgressLocation as MainProgressLocation } from 'vs/platform/progress/common/progress'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; -import { getPrivateApiFor, TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; +import { getPrivateApiFor } from 'vs/workbench/api/common/extHostTestingPrivateApi'; import { SaveReason } from 'vs/workbench/common/editor'; +import { IViewBadge } from 'vs/workbench/common/views'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import * as search from 'vs/workbench/contrib/search/common/search'; -import { CoverageDetails, denamespaceTestTag, DetailType, ICoveredCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestItemContext, ITestTag, namespaceTestTag, TestMessageType, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { CoverageDetails, denamespaceTestTag, DetailType, ICoveredCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestTag, namespaceTestTag, TestMessageType, TestResultItem } from 'vs/workbench/contrib/testing/common/testTypes'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import type * as vscode from 'vscode'; @@ -557,6 +558,15 @@ export namespace TextEdit { } } +export namespace SnippetTextEdit { + export function from(edit: vscode.SnippetTextEdit): languages.SnippetTextEdit { + return { + range: Range.from(edit.range), + snippet: edit.snippet.value + }; + } +} + export namespace WorkspaceEdit { export interface IVersionInformationProvider { @@ -1655,8 +1665,7 @@ export namespace NotebookExclusiveDocumentPattern { if (!ep) { return false; } - - return !!ep.include && !!ep.exclude; + return !isUndefinedOrNull(ep.include) && !isUndefinedOrNull(ep.exclude); } } @@ -1735,7 +1744,7 @@ export namespace TestTag { export namespace TestItem { export type Raw = vscode.TestItem; - export function from(item: TestItemImpl): ITestItem { + export function from(item: vscode.TestItem): ITestItem { const ctrlId = getPrivateApiFor(item).controllerId; return { extId: TestId.fromExtHostTestItem(item, ctrlId).toString(), @@ -1750,7 +1759,7 @@ export namespace TestItem { }; } - export function toPlain(item: ITestItem.Serialized): Omit<vscode.TestItem, 'children' | 'invalidate' | 'discoverChildren'> { + export function toPlain(item: ITestItem.Serialized): vscode.TestItem { return { parent: undefined, error: undefined, @@ -1761,34 +1770,21 @@ export namespace TestItem { const { tagId } = TestTag.denamespace(t); return new types.TestTag(tagId); }), + children: { + add: () => { }, + delete: () => { }, + forEach: () => { }, + get: () => undefined, + replace: () => { }, + size: 0, + }, range: Range.to(item.range || undefined), - invalidateResults: () => undefined, canResolveChildren: false, busy: false, description: item.description || undefined, sortText: item.sortText || undefined, }; } - - function to(item: ITestItem): TestItemImpl { - const testId = TestId.fromString(item.extId); - const testItem = new TestItemImpl(testId.controllerId, testId.localId, item.label, URI.revive(item.uri) || undefined); - testItem.range = Range.to(item.range || undefined); - testItem.description = item.description || undefined; - testItem.sortText = item.sortText || undefined; - return testItem; - } - - export function toItemFromContext(context: ITestItemContext): TestItemImpl { - let node: TestItemImpl | undefined; - for (const test of context.tests) { - const next = to(test.item); - getPrivateApiFor(next).parent = node; - node = next; - } - - return node!; - } } export namespace TestTag { @@ -1935,3 +1931,16 @@ export namespace TypeHierarchyItem { }; } } + +export namespace ViewBadge { + export function from(badge: vscode.ViewBadge | undefined): IViewBadge | undefined { + if (!badge) { + return undefined; + } + + return { + value: badge.value, + tooltip: badge.tooltip + }; + } +} diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 700ab99bffb..ca19cdc010c 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -598,6 +598,17 @@ export class TextEdit { } } +export class SnippetTextEdit implements vscode.SnippetTextEdit { + + range: vscode.Range; + snippet: vscode.SnippetString; + + constructor(range: Range, snippet: SnippetString) { + this.range = range; + this.snippet = snippet; + } +} + export interface IFileOperationOptions { overwrite?: boolean; ignoreIfExists?: boolean; @@ -1826,7 +1837,7 @@ export class TerminalProfile implements vscode.TerminalProfile { public options: vscode.TerminalOptions | vscode.ExtensionTerminalOptions ) { if (typeof options !== 'object') { - illegalArgument('options'); + throw illegalArgument('options'); } } } diff --git a/src/vs/workbench/api/common/extHostWebviewView.ts b/src/vs/workbench/api/common/extHostWebviewView.ts index ed711d56d9a..884e05f44a3 100644 --- a/src/vs/workbench/api/common/extHostWebviewView.ts +++ b/src/vs/workbench/api/common/extHostWebviewView.ts @@ -8,6 +8,8 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostWebview, ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { ViewBadge } from 'vs/workbench/api/common/extHostTypeConverters'; import type * as vscode from 'vscode'; import * as extHostProtocol from './extHost.protocol'; import * as extHostTypes from './extHostTypes'; @@ -19,11 +21,13 @@ class ExtHostWebviewView extends Disposable implements vscode.WebviewView { readonly #viewType: string; readonly #webview: ExtHostWebview; + readonly #extension: IExtensionDescription; #isDisposed = false; #isVisible: boolean; #title: string | undefined; #description: string | undefined; + #badge: vscode.ViewBadge | undefined; constructor( handle: extHostProtocol.WebviewHandle, @@ -31,6 +35,7 @@ class ExtHostWebviewView extends Disposable implements vscode.WebviewView { viewType: string, title: string | undefined, webview: ExtHostWebview, + extension: IExtensionDescription, isVisible: boolean, ) { super(); @@ -40,6 +45,7 @@ class ExtHostWebviewView extends Disposable implements vscode.WebviewView { this.#handle = handle; this.#proxy = proxy; this.#webview = webview; + this.#extension = extension; this.#isVisible = isVisible; } @@ -103,6 +109,25 @@ class ExtHostWebviewView extends Disposable implements vscode.WebviewView { this.#onDidChangeVisibility.fire(); } + public get badge(): vscode.ViewBadge | undefined { + this.assertNotDisposed(); + checkProposedApiEnabled(this.#extension, 'badges'); + return this.#badge; + } + + public set badge(badge: vscode.ViewBadge | undefined) { + this.assertNotDisposed(); + checkProposedApiEnabled(this.#extension, 'badges'); + + if (badge?.value === this.#badge?.value && + badge?.tooltip === this.#badge?.tooltip) { + return; + } + + this.#badge = ViewBadge.from(badge); + this.#proxy.$setWebviewViewBadge(this.#handle, badge); + } + public show(preserveFocus?: boolean): void { this.assertNotDisposed(); this.#proxy.$show(this.#handle, !!preserveFocus); @@ -172,7 +197,7 @@ export class ExtHostWebviewViews implements extHostProtocol.ExtHostWebviewViewsS const { provider, extension } = entry; const webview = this._extHostWebview.createNewWebview(webviewHandle, { /* todo */ }, extension); - const revivedView = new ExtHostWebviewView(webviewHandle, this._proxy, viewType, title, webview, true); + const revivedView = new ExtHostWebviewView(webviewHandle, this._proxy, viewType, title, webview, extension, true); this._webviewViews.set(webviewHandle, revivedView); diff --git a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts index 233985e12c3..a9ff3908319 100644 --- a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts +++ b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts @@ -7,7 +7,7 @@ import type * as vscode from 'vscode'; import assert = require('assert'); import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; -import { IEditorTabDto, MainThreadEditorTabsShape, TabInputKind, TextInputDto } from 'vs/workbench/api/common/extHost.protocol'; +import { IEditorTabDto, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextInputDto } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; import { TextTabInput } from 'vs/workbench/api/common/extHostTypes'; @@ -109,7 +109,7 @@ suite('ExtHostEditorTabs', function () { ); let count = 0; - extHostEditorTabs.tabGroups.onDidChangeTabGroup(() => count++); + extHostEditorTabs.tabGroups.onDidChangeTabGroups(() => count++); assert.strictEqual(count, 0); @@ -155,68 +155,6 @@ suite('ExtHostEditorTabs', function () { assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup, first); }); - // TODO @lramos15 Change this test because now it only fires when id changes - test.skip('onDidChangeActiveTabGroup fires properly', function () { - const extHostEditorTabs = new ExtHostEditorTabs( - SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() { - // override/implement $moveTab or $closeTab - }) - ); - - let count = 0; - let activeTabGroupFromEvent: vscode.TabGroup | undefined = undefined; - extHostEditorTabs.tabGroups.onDidChangeActiveTabGroup((tabGroup) => { - count++; - activeTabGroupFromEvent = tabGroup; - }); - - - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 0); - assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup, undefined); - assert.strictEqual(count, 0); - const tabModel = [{ - isActive: true, - viewColumn: 0, - groupId: 12, - tabs: [], - activeTab: undefined - }]; - extHostEditorTabs.$acceptEditorTabModel(tabModel); - assert.ok(extHostEditorTabs.tabGroups.activeTabGroup); - let activeTabGroup: vscode.TabGroup = extHostEditorTabs.tabGroups.activeTabGroup; - assert.strictEqual(count, 1); - assert.strictEqual(activeTabGroup, activeTabGroupFromEvent); - // Firing again with same model shouldn't cause a change - extHostEditorTabs.$acceptEditorTabModel(tabModel); - assert.strictEqual(count, 1); - // Changing a property should fire a change - tabModel[0].viewColumn = 1; - extHostEditorTabs.$acceptEditorTabModel(tabModel); - assert.strictEqual(count, 2); - activeTabGroup = extHostEditorTabs.tabGroups.activeTabGroup; - assert.strictEqual(activeTabGroup, activeTabGroupFromEvent); - // Changing the active tab group should fire a change - tabModel[0].isActive = false; - tabModel.push({ - isActive: true, - viewColumn: 0, - groupId: 13, - tabs: [], - activeTab: undefined - }); - extHostEditorTabs.$acceptEditorTabModel(tabModel); - assert.strictEqual(count, 3); - activeTabGroup = extHostEditorTabs.tabGroups.activeTabGroup; - assert.strictEqual(activeTabGroup, activeTabGroupFromEvent); - - // Empty tab model should fire a change and return undefined - extHostEditorTabs.$acceptEditorTabModel([]); - assert.strictEqual(count, 4); - activeTabGroup = extHostEditorTabs.tabGroups.activeTabGroup; - assert.strictEqual(activeTabGroup, undefined); - assert.strictEqual(activeTabGroup, activeTabGroupFromEvent); - }); - test('Ensure reference stability', function () { const extHostEditorTabs = new ExtHostEditorTabs( @@ -248,7 +186,12 @@ suite('ExtHostEditorTabs', function () { const tabDto2: IEditorTabDto = { ...tabDto, isDirty: false }; // Accept a simple update - extHostEditorTabs.$acceptTabUpdate(12, tabDto2); + extHostEditorTabs.$acceptTabOperation({ + kind: TabModelOperationKind.TAB_UPDATE, + index: 0, + tabDto: tabDto2, + groupId: 12 + }); all = extHostEditorTabs.tabGroups.groups.map(group => group.tabs).flat(); assert.strictEqual(all.length, 1); @@ -306,7 +249,12 @@ suite('ExtHostEditorTabs', function () { assert.strictEqual(activeTab1?.kind?.uri.toString(), URI.revive(dtoAAAResource)?.toString()); assert.strictEqual(activeTab1?.isActive, true); - extHostEditorTabs.$acceptTabUpdate(12, { ...tabDtoBBB, isActive: true }); /// BBB is now active + extHostEditorTabs.$acceptTabOperation({ + groupId: 12, + index: 1, + kind: TabModelOperationKind.TAB_UPDATE, + tabDto: { ...tabDtoBBB, isActive: true } /// BBB is now active + }); const activeTab2 = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab; assert.ok(activeTab2?.kind instanceof TextTabInput); @@ -339,7 +287,7 @@ suite('ExtHostEditorTabs', function () { }); assert.throws(() => { // @ts-expect-error write to readonly prop - extHostEditorTabs.tabGroups.onDidChangeTabGroup = undefined; + extHostEditorTabs.tabGroups.onDidChangeTabGroups = undefined; }); }); @@ -350,6 +298,7 @@ suite('ExtHostEditorTabs', function () { // override/implement $moveTab or $closeTab override async $closeTab(tabIds: string[], preserveFocus?: boolean) { closedTabIds.push(tabIds); + return true; } }) ); @@ -387,6 +336,7 @@ suite('ExtHostEditorTabs', function () { // override/implement $moveTab or $closeTab override async $closeTab(tabIds: string[], preserveFocus?: boolean) { closedTabIds.push(tabIds); + return true; } }) ); @@ -411,11 +361,16 @@ suite('ExtHostEditorTabs', function () { const tab = extHostEditorTabs.tabGroups.groups[0].tabs[0]; - const p = new Promise<vscode.Tab>(resolve => extHostEditorTabs.tabGroups.onDidChangeTab(resolve)); + const p = new Promise<vscode.Tab[]>(resolve => extHostEditorTabs.tabGroups.onDidChangeTabs(resolve)); - extHostEditorTabs.$acceptTabUpdate(12, { ...tabDto, label: 'NEW LABEL' }); + extHostEditorTabs.$acceptTabOperation({ + groupId: 12, + index: 0, + kind: TabModelOperationKind.TAB_UPDATE, + tabDto: { ...tabDto, label: 'NEW LABEL' } + }); - const changedTab = await p; + const changedTab = (await p)[0]; assert.ok(tab === changedTab); assert.strictEqual(changedTab.label, 'NEW LABEL'); @@ -464,8 +419,18 @@ suite('ExtHostEditorTabs', function () { // Switching active tab works tab1.isActive = false; tab2.isActive = true; - extHostEditorTabs.$acceptTabUpdate(12, tab1); - extHostEditorTabs.$acceptTabUpdate(12, tab2); + extHostEditorTabs.$acceptTabOperation({ + groupId: 12, + index: 0, + kind: TabModelOperationKind.TAB_UPDATE, + tabDto: tab1 + }); + extHostEditorTabs.$acceptTabOperation({ + groupId: 12, + index: 1, + kind: TabModelOperationKind.TAB_UPDATE, + tabDto: tab2 + }); assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, extHostEditorTabs.tabGroups.activeTabGroup?.tabs[1]); //Closing tabs out works @@ -491,4 +456,81 @@ suite('ExtHostEditorTabs', function () { assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 0); assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, undefined); }); + + test('Tab operations patches open and close correctly', function () { + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() { + // override/implement $moveTab or $closeTab + }) + ); + + const tab1: IEditorTabDto = createTabDto({ + id: 'uniquestring', + isActive: true, + label: 'label1', + }); + + const tab2: IEditorTabDto = createTabDto({ + isActive: false, + id: 'uniquestring2', + label: 'label2', + }); + + const tab3: IEditorTabDto = createTabDto({ + isActive: false, + id: 'uniquestring3', + label: 'label3', + }); + + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [tab1, tab2, tab3] + }]); + + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 3); + + // Close tab 2 + extHostEditorTabs.$acceptTabOperation({ + groupId: 12, + index: 1, + kind: TabModelOperationKind.TAB_CLOSE, + tabDto: tab2 + }); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 2); + + // Close active tab and update tab 3 to be active + extHostEditorTabs.$acceptTabOperation({ + groupId: 12, + index: 0, + kind: TabModelOperationKind.TAB_CLOSE, + tabDto: tab1 + }); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 1); + tab3.isActive = true; + extHostEditorTabs.$acceptTabOperation({ + groupId: 12, + index: 0, + kind: TabModelOperationKind.TAB_UPDATE, + tabDto: tab3 + }); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.groups[0]?.activeTab?.label, 'label3'); + + // Open tab 2 back + extHostEditorTabs.$acceptTabOperation({ + groupId: 12, + index: 1, + kind: TabModelOperationKind.TAB_OPEN, + tabDto: tab2 + }); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 2); + assert.strictEqual(extHostEditorTabs.tabGroups.groups[0]?.tabs[1]?.label, 'label2'); + }); }); diff --git a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts index 94b8161ce5f..e4f6d0f2a13 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts @@ -60,7 +60,7 @@ suite('NotebookCell#Document', function () { } }; extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, extHostDocuments, extHostStoragePaths); - extHostNotebookDocuments = new ExtHostNotebookDocuments(new NullLogService(), extHostNotebooks); + extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock<vscode.NotebookContentProvider>() { // async openNotebook() { } @@ -146,12 +146,13 @@ suite('NotebookCell#Document', function () { test('cell document is vscode.TextDocument after changing it', async function () { const p = new Promise<void>((resolve, reject) => { - extHostNotebooks.onDidChangeNotebookCells(e => { + + extHostNotebookDocuments.onDidChangeNotebookDocument(e => { try { - assert.strictEqual(e.changes.length, 1); - assert.strictEqual(e.changes[0].items.length, 2); + assert.strictEqual(e.contentChanges.length, 1); + assert.strictEqual(e.contentChanges[0].addedCells.length, 2); - const [first, second] = e.changes[0].items; + const [first, second] = e.contentChanges[0].addedCells; const doc1 = extHostDocuments.getAllDocumentData().find(data => isEqual(data.document.uri, first.document.uri)); assert.ok(doc1); @@ -167,6 +168,7 @@ suite('NotebookCell#Document', function () { reject(err); } }); + }); extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ @@ -317,7 +319,7 @@ suite('NotebookCell#Document', function () { test('ERR MISSING extHostDocument for notebook cell: #116711', async function () { - const p = Event.toPromise(extHostNotebooks.onDidChangeNotebookCells); + const p = Event.toPromise(extHostNotebookDocuments.onDidChangeNotebookDocument); // DON'T call this, make sure the cell-documents have not been created yet // assert.strictEqual(notebook.notebookDocument.cellCount, 2); @@ -350,14 +352,14 @@ suite('NotebookCell#Document', function () { const event = await p; - assert.strictEqual(event.document === notebook.apiNotebook, true); - assert.strictEqual(event.changes.length, 1); - assert.strictEqual(event.changes[0].deletedCount, 2); - assert.strictEqual(event.changes[0].deletedItems[0].document.isClosed, true); - assert.strictEqual(event.changes[0].deletedItems[1].document.isClosed, true); - assert.strictEqual(event.changes[0].items.length, 2); - assert.strictEqual(event.changes[0].items[0].document.isClosed, false); - assert.strictEqual(event.changes[0].items[1].document.isClosed, false); + assert.strictEqual(event.notebook === notebook.apiNotebook, true); + assert.strictEqual(event.contentChanges.length, 1); + assert.strictEqual(event.contentChanges[0].range.end - event.contentChanges[0].range.start, 2); + assert.strictEqual(event.contentChanges[0].removedCells[0].document.isClosed, true); + assert.strictEqual(event.contentChanges[0].removedCells[1].document.isClosed, true); + assert.strictEqual(event.contentChanges[0].addedCells.length, 2); + assert.strictEqual(event.contentChanges[0].addedCells[0].document.isClosed, false); + assert.strictEqual(event.contentChanges[0].addedCells[1].document.isClosed, false); }); @@ -407,7 +409,7 @@ suite('NotebookCell#Document', function () { extHostNotebookDocuments.$acceptModelChanged(notebook.uri, new SerializableObjectWithBuffers({ versionId: 12, rawEvents: [{ - kind: NotebookCellsChangeType.ChangeLanguage, + kind: NotebookCellsChangeType.ChangeCellLanguage, index: 0, language: 'fooLang' }] @@ -506,7 +508,7 @@ suite('NotebookCell#Document', function () { extHostNotebookDocuments.$acceptModelChanged(notebook.uri, new SerializableObjectWithBuffers({ versionId: 12, rawEvents: [{ - kind: NotebookCellsChangeType.ChangeLanguage, + kind: NotebookCellsChangeType.ChangeCellLanguage, index: 0, language: 'fooLang' }] diff --git a/src/vs/workbench/api/test/browser/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/api/test/browser/extHostNotebookConcatDocument.test.ts index b2bddc7f279..6267d249941 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebookConcatDocument.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebookConcatDocument.test.ts @@ -56,7 +56,7 @@ suite('NotebookConcatDocument', function () { } }; extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, extHostDocuments, extHostStoragePaths); - extHostNotebookDocuments = new ExtHostNotebookDocuments(new NullLogService(), extHostNotebooks); + extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock<vscode.NotebookContentProvider>() { // async openNotebook() { } @@ -93,7 +93,7 @@ suite('NotebookConcatDocument', function () { }); test('empty', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.apiNotebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); assert.strictEqual(doc.getText(), ''); assert.strictEqual(doc.version, 0); @@ -156,7 +156,7 @@ suite('NotebookConcatDocument', function () { assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.apiNotebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); assert.strictEqual(doc.contains(cellUri1), true); assert.strictEqual(doc.contains(cellUri2), true); @@ -194,7 +194,7 @@ suite('NotebookConcatDocument', function () { assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.apiNotebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); @@ -207,7 +207,7 @@ suite('NotebookConcatDocument', function () { test('location, position mapping, cell changes', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.apiNotebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); // UPDATE 1 extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ @@ -284,7 +284,7 @@ suite('NotebookConcatDocument', function () { test('location, position mapping, cell-document changes', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.apiNotebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); // UPDATE 1 extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ @@ -374,9 +374,9 @@ suite('NotebookConcatDocument', function () { ] }), false); - const mixedDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.apiNotebook, undefined); - const fooLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.apiNotebook, 'fooLang'); - const barLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.apiNotebook, 'barLang'); + const mixedDoc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); + const fooLangDoc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, 'fooLang'); + const barLangDoc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, 'barLang'); assertLines(mixedDoc, 'fooLang-document', 'barLang-document'); assertLines(fooLangDoc, 'fooLang-document'); @@ -448,7 +448,7 @@ suite('NotebookConcatDocument', function () { assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.apiNotebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); assertOffsetAtPosition(doc, 0, { line: 0, character: 0 }); @@ -505,7 +505,7 @@ suite('NotebookConcatDocument', function () { assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.apiNotebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); assertLocationAtPosition(doc, { line: 0, character: 0 }, { uri: notebook.apiNotebook.cellAt(0).document.uri, line: 0, character: 0 }); @@ -554,7 +554,7 @@ suite('NotebookConcatDocument', function () { assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 3); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.apiNotebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!', 'Three', 'Drei', 'Drüü'); assert.strictEqual(doc.getText(new Range(0, 0, 0, 0)), ''); @@ -593,7 +593,7 @@ suite('NotebookConcatDocument', function () { assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.apiNotebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); diff --git a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts index 4c4bd0a21ce..90edc6979d2 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts @@ -98,7 +98,7 @@ suite('NotebookKernel', function () { extHostCommands = new ExtHostCommands(rpcProtocol, new NullLogService()); extHostNotebooks = new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostStoragePaths); - extHostNotebookDocuments = new ExtHostNotebookDocuments(new NullLogService(), extHostNotebooks); + extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); extHostNotebooks.$acceptDocumentAndEditorsDelta(new SerializableObjectWithBuffers({ addedDocuments: [{ diff --git a/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/src/vs/workbench/api/test/browser/extHostTesting.test.ts index 85cfba00a3f..c4ce107acf1 100644 --- a/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -11,12 +11,11 @@ import { URI } from 'vs/base/common/uri'; import { mockObject, MockObject } from 'vs/base/test/common/mock'; import { MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; import { TestRunCoordinator, TestRunDto, TestRunProfileImpl } from 'vs/workbench/api/common/extHostTesting'; +import { ExtHostTestItemCollection, TestItemImpl } from 'vs/workbench/api/common/extHostTestItem'; import * as convert from 'vs/workbench/api/common/extHostTypeConverters'; import { Location, Position, Range, TestMessage, TestResultState, TestRunProfileKind, TestRunRequest as TestRunRequestImpl, TestTag } from 'vs/workbench/api/common/extHostTypes'; -import { TestDiffOpType, TestItemExpandState, TestMessageType } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestDiffOpType, TestItemExpandState, TestMessageType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; -import { TestItemImpl, testStubs } from 'vs/workbench/contrib/testing/test/common/testStubs'; -import { TestSingleUseCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; import type { TestItem, TestRunRequest } from 'vscode'; const simplify = (item: TestItem) => ({ @@ -62,9 +61,28 @@ const assertTreesEqual = (a: TestItemImpl | undefined, b: TestItemImpl | undefin // } suite('ExtHost Testing', () => { - let single: TestSingleUseCollection; + class TestExtHostTestItemCollection extends ExtHostTestItemCollection { + public setDiff(diff: TestsDiff) { + this.diff = diff; + } + } + + let single: TestExtHostTestItemCollection; setup(() => { - single = testStubs.nested(); + single = new TestExtHostTestItemCollection('ctrlId', 'root'); + single.resolveHandler = item => { + if (item === undefined) { + const a = new TestItemImpl('ctrlId', 'id-a', 'a', URI.file('/')); + a.canResolveChildren = true; + const b = new TestItemImpl('ctrlId', 'id-b', 'b', URI.file('/')); + single.root.children.add(a); + single.root.children.add(b); + } else if (item.id === 'id-a') { + item.children.add(new TestItemImpl('ctrlId', 'id-aa', 'aa', URI.file('/'))); + item.children.add(new TestItemImpl('ctrlId', 'id-ab', 'ab', URI.file('/'))); + } + }; + single.onDidGenerateDiff(d => single.setDiff(d /* don't clear during testing */)); }); @@ -186,8 +204,8 @@ suite('ExtHost Testing', () => { single.root.children.get('id-a')!.children.add(child); assert.deepStrictEqual(single.collectDiff(), [ - { op: TestDiffOpType.AddTag, tag: { ctrlLabel: 'root', id: 'ctrlId\0tag1' } }, - { op: TestDiffOpType.AddTag, tag: { ctrlLabel: 'root', id: 'ctrlId\0tag2' } }, + { op: TestDiffOpType.AddTag, tag: { id: 'ctrlId\0tag1' } }, + { op: TestDiffOpType.AddTag, tag: { id: 'ctrlId\0tag2' } }, { op: TestDiffOpType.Add, item: { controllerId: 'ctrlId', @@ -200,7 +218,7 @@ suite('ExtHost Testing', () => { child.tags = [tag2, tag3]; assert.deepStrictEqual(single.collectDiff(), [ - { op: TestDiffOpType.AddTag, tag: { ctrlLabel: 'root', id: 'ctrlId\0tag3' } }, + { op: TestDiffOpType.AddTag, tag: { id: 'ctrlId\0tag3' } }, { op: TestDiffOpType.Update, item: { extId: new TestId(['ctrlId', 'id-a', 'id-ac']).toString(), diff --git a/src/vs/workbench/api/test/browser/extHostWebview.test.ts b/src/vs/workbench/api/test/browser/extHostWebview.test.ts index a8db2073176..cce4f74d246 100644 --- a/src/vs/workbench/api/test/browser/extHostWebview.test.ts +++ b/src/vs/workbench/api/test/browser/extHostWebview.test.ts @@ -133,12 +133,12 @@ suite('ExtHostWebview', () => { const webviewUri = webview.webview.asWebviewUri(sourceUri); assert.strictEqual( webviewUri.toString(), - `https://vscode-remote%2Bssh-002dremote-002blocalhost-003dfoo-002fbar.vscode-resource.vscode-webview.net/Users/cody/x.png`, + `https://vscode-remote%2Bssh-002dremote-002blocalhost-003dfoo-002fbar.vscode-resource.vscode-cdn.net/Users/cody/x.png`, 'Check transform'); assert.strictEqual( decodeAuthority(webviewUri.authority), - `vscode-remote+${authority}.vscode-resource.vscode-webview.net`, + `vscode-remote+${authority}.vscode-resource.vscode-cdn.net`, 'Check decoded authority' ); }); @@ -156,12 +156,12 @@ suite('ExtHostWebview', () => { const webviewUri = webview.webview.asWebviewUri(sourceUri); assert.strictEqual( webviewUri.toString(), - `https://vscode-remote%2Blocalhost-003a8080.vscode-resource.vscode-webview.net/Users/cody/x.png`, + `https://vscode-remote%2Blocalhost-003a8080.vscode-resource.vscode-cdn.net/Users/cody/x.png`, 'Check transform'); assert.strictEqual( decodeAuthority(webviewUri.authority), - `vscode-remote+${authority}.vscode-resource.vscode-webview.net`, + `vscode-remote+${authority}.vscode-resource.vscode-cdn.net`, 'Check decoded authority' ); }); diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts index adfaa71f27b..3ecd7f8a546 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts @@ -105,7 +105,6 @@ suite('MainThreadDocumentsAndEditors', () => { fileService, null!, editorGroupService, - null!, new class extends mock<IPaneCompositePartService>() implements IPaneCompositePartService { override onDidPaneCompositeOpen = Event.None; override onDidPaneCompositeClose = Event.None; diff --git a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts index 4e65fc009ee..6e509185ca3 100644 --- a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts @@ -4,16 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { MainThreadDocumentsAndEditors } from 'vs/workbench/api/browser/mainThreadDocumentsAndEditors'; -import { SingleProxyRPCProtocol, TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; +import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { ModelService } from 'vs/editor/common/services/modelService'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { ExtHostDocumentsAndEditorsShape, ExtHostContext, ExtHostDocumentsShape, IWorkspaceTextEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; +import { IWorkspaceTextEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; import { mock } from 'vs/base/test/common/mock'; import { Event } from 'vs/base/common/event'; -import { MainThreadTextEditors } from 'vs/workbench/api/browser/mainThreadEditors'; import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; @@ -57,6 +55,7 @@ import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/te import { LanguageService } from 'vs/editor/common/services/languageService'; import { LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; +import { MainThreadBulkEdits } from 'vs/workbench/api/browser/mainThreadBulkEdits'; suite('MainThreadEditors', () => { @@ -64,7 +63,8 @@ suite('MainThreadEditors', () => { const resource = URI.parse('foo:bar'); let modelService: IModelService; - let editors: MainThreadTextEditors; + + let bulkEdits: MainThreadBulkEdits; const movedResources = new Map<URI, URI>(); const copiedResources = new Map<URI, URI>(); @@ -185,19 +185,7 @@ suite('MainThreadEditors', () => { const instaService = new InstantiationService(services); - const rpcProtocol = new TestRPCProtocol(); - rpcProtocol.set(ExtHostContext.ExtHostDocuments, new class extends mock<ExtHostDocumentsShape>() { - override $acceptModelChanged(): void { - } - }); - rpcProtocol.set(ExtHostContext.ExtHostDocumentsAndEditors, new class extends mock<ExtHostDocumentsAndEditorsShape>() { - override $acceptDocumentsAndEditorsDelta(): void { - } - }); - - const documentAndEditor = instaService.createInstance(MainThreadDocumentsAndEditors, rpcProtocol); - - editors = instaService.createInstance(MainThreadTextEditors, documentAndEditor, SingleProxyRPCProtocol(null)); + bulkEdits = instaService.createInstance(MainThreadBulkEdits, SingleProxyRPCProtocol(null)); }); teardown(() => { @@ -221,7 +209,7 @@ suite('MainThreadEditors', () => { // Act as if the user edited the model model.applyEdits([EditOperation.insert(new Position(0, 0), 'something')]); - return editors.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit] }).then((result) => { + return bulkEdits.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit] }).then((result) => { assert.strictEqual(result, false); }); }); @@ -249,11 +237,11 @@ suite('MainThreadEditors', () => { } }; - let p1 = editors.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit1] }).then((result) => { + let p1 = bulkEdits.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit1] }).then((result) => { // first edit request succeeds assert.strictEqual(result, true); }); - let p2 = editors.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit2] }).then((result) => { + let p2 = bulkEdits.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit2] }).then((result) => { // second edit request fails assert.strictEqual(result, false); }); @@ -261,7 +249,7 @@ suite('MainThreadEditors', () => { }); test(`applyWorkspaceEdit with only resource edit`, () => { - return editors.$tryApplyWorkspaceEdit({ + return bulkEdits.$tryApplyWorkspaceEdit({ edits: [ { _type: WorkspaceEditType.File, oldUri: resource, newUri: resource, options: undefined }, { _type: WorkspaceEditType.File, oldUri: undefined, newUri: resource, options: undefined }, diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index 4927fc3be3e..68c7c834910 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -55,7 +55,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { return extensionDescription.browser; } - protected async _loadCommonJSModule<T>(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> { + protected async _loadCommonJSModule<T extends object | undefined>(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> { module = module.with({ path: ensureSuffix(module.path, '.js') }); if (extensionId) { performance.mark(`code/extHost/willFetchExtensionCode/${extensionId.value}`); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 53f5e36f822..1c72df3701b 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -41,6 +41,7 @@ import { ITreeDragOverReaction } from 'vs/base/browser/ui/tree/tree'; import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; import { HTMLFileSystemProvider } from 'vs/platform/files/browser/htmlFileSystemProvider'; import { DeferredPromise } from 'vs/base/common/async'; +import { Registry } from 'vs/platform/registry/common/platform'; //#region Editor / Resources DND @@ -132,19 +133,6 @@ export async function extractEditorsDropData(accessor: ServicesAccessor, e: Drag } } - // Check for terminals transfer - const terminals = e.dataTransfer.getData(DataTransfers.TERMINALS); - if (terminals) { - try { - const terminalEditors: string[] = JSON.parse(terminals); - for (const terminalEditor of terminalEditors) { - editors.push({ resource: URI.parse(terminalEditor) }); - } - } catch (error) { - // Invalid transfer - } - } - // Web: Check for file transfer if (isWeb && containsDragType(e, DataTransfers.FILES)) { const files = e.dataTransfer.items; @@ -156,6 +144,19 @@ export async function extractEditorsDropData(accessor: ServicesAccessor, e: Drag } } } + + // Workbench contributions + const contributions = Registry.as<IDragAndDropContributionRegistry>(Extensions.DragAndDropContribution).getAll(); + for (const contribution of contributions) { + const data = e.dataTransfer.getData(contribution.dataFormatKey); + if (data) { + try { + editors.push(...contribution.getEditorInputs(data)); + } catch (error) { + // Invalid transfer + } + } + } } return editors; @@ -502,10 +503,10 @@ export function fillEditorsDragData(accessor: ServicesAccessor, resourcesOrEdito event.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(files.map(({ resource }) => resource.toString()))); } - // Terminal URI - const terminalResources = resources.filter(({ resource }) => resource.scheme === Schemas.vscodeTerminal); - if (terminalResources.length) { - event.dataTransfer.setData(DataTransfers.TERMINALS, JSON.stringify(terminalResources.map(({ resource }) => resource.toString()))); + // Contributions + const contributions = Registry.as<IDragAndDropContributionRegistry>(Extensions.DragAndDropContribution).getAll(); + for (const contribution of contributions) { + contribution.setData(resources, event); } // Editors: enables cross window DND of editors @@ -590,6 +591,49 @@ export function fillEditorsDragData(accessor: ServicesAccessor, resourcesOrEdito //#endregion +//#region DND contributions + +export interface IDragAndDropContributionRegistry { + /** + * Registers a drag and drop contribution. + */ + register(contribution: IDragAndDropContribution): void; + + /** + * Returns all registered drag and drop contributions. + */ + getAll(): IterableIterator<IDragAndDropContribution>; +} + +export interface IDragAndDropContribution { + readonly dataFormatKey: string; + getEditorInputs(data: string): IDraggedResourceEditorInput[]; + setData(resources: IResourceStat[], event: DragMouseEvent | DragEvent): void; +} + +class DragAndDropContributionRegistry implements IDragAndDropContributionRegistry { + private readonly _contributions = new Map<string, IDragAndDropContribution>(); + + register(contribution: IDragAndDropContribution): void { + if (this._contributions.has(contribution.dataFormatKey)) { + throw new Error(`A drag and drop contributiont with key '${contribution.dataFormatKey}' was already registered.`); + } + this._contributions.set(contribution.dataFormatKey, contribution); + } + + getAll(): IterableIterator<IDragAndDropContribution> { + return this._contributions.values(); + } +} + +export const Extensions = { + DragAndDropContribution: 'workbench.contributions.dragAndDrop' +}; + +Registry.add(Extensions.DragAndDropContribution, new DragAndDropContributionRegistry()); + +//#endregion + //#region DND Utilities /** diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 10c2190c88a..10fd58c5c0b 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -262,7 +262,7 @@ export function whenEditorClosed(accessor: ServicesAccessor, resources: URI[]): //#region ARIA -export function computeEditorAriaLabel(input: EditorInput, index: number | undefined, group: IEditorGroup | undefined, groupCount: number): string { +export function computeEditorAriaLabel(input: EditorInput, index: number | undefined, group: IEditorGroup | undefined, groupCount: number | undefined): string { let ariaLabel = input.getAriaLabel(); if (group && !group.isPinned(input)) { ariaLabel = localize('preview', "{0}, preview", ariaLabel); @@ -275,7 +275,7 @@ export function computeEditorAriaLabel(input: EditorInput, index: number | undef // Apply group information to help identify in // which group we are (only if more than one group // is actually opened) - if (group && groupCount > 1) { + if (group && typeof groupCount === 'number' && groupCount > 1) { ariaLabel = `${ariaLabel}, ${group.ariaLabel}`; } diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index ab172e9c0f5..0995d4e6e48 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -523,7 +523,7 @@ export class CloseLeftEditorsInGroupAction extends Action { override async run(context?: IEditorIdentifier): Promise<void> { const { group, editor } = this.getTarget(context); if (group && editor) { - return group.closeEditors({ direction: CloseDirection.LEFT, except: editor, excludeSticky: true }); + await group.closeEditors({ direction: CloseDirection.LEFT, except: editor, excludeSticky: true }); } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 3668557547d..cb67d57d434 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -816,7 +816,7 @@ function registerCloseEditorCommands() { .map(editor => typeof editor.editorIndex === 'number' ? group.getEditorByIndex(editor.editorIndex) : group.activeEditor)) .filter(editor => !keepStickyEditors || !group.isSticky(editor)); - return group.closeEditors(editorsToClose, { preserveFocus: context?.preserveFocus }); + await group.closeEditors(editorsToClose, { preserveFocus: context?.preserveFocus }); } })); } @@ -881,7 +881,7 @@ function registerCloseEditorCommands() { handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { return Promise.all(getEditorsContext(accessor, resourceOrContext, context).groups.map(async group => { if (group) { - return group.closeEditors({ savedOnly: true, excludeSticky: true }, { preserveFocus: context?.preserveFocus }); + await group.closeEditors({ savedOnly: true, excludeSticky: true }, { preserveFocus: context?.preserveFocus }); } })); } @@ -909,7 +909,7 @@ function registerCloseEditorCommands() { } } - return group.closeEditors(editorsToClose, { preserveFocus: context?.preserveFocus }); + await group.closeEditors(editorsToClose, { preserveFocus: context?.preserveFocus }); } })); } @@ -929,7 +929,7 @@ function registerCloseEditorCommands() { group.pinEditor(group.activeEditor); } - return group.closeEditors({ direction: CloseDirection.RIGHT, except: editor, excludeSticky: true }, { preserveFocus: context?.preserveFocus }); + await group.closeEditors({ direction: CloseDirection.RIGHT, except: editor, excludeSticky: true }, { preserveFocus: context?.preserveFocus }); } } }); diff --git a/src/vs/workbench/browser/parts/editor/editorConfiguration.ts b/src/vs/workbench/browser/parts/editor/editorConfiguration.ts index 97af9e0e507..986eb4f0a18 100644 --- a/src/vs/workbench/browser/parts/editor/editorConfiguration.ts +++ b/src/vs/workbench/browser/parts/editor/editorConfiguration.ts @@ -100,8 +100,9 @@ export class DynamicEditorResolverConfigurations extends Disposable implements I properties: { 'workbench.editor.defaultBinaryEditor': { type: 'string', + default: '', // This allows for intellisense autocompletion - enum: binaryEditorCandidates, + enum: [...binaryEditorCandidates, ''], description: localize('workbench.editor.defaultBinaryEditor', "The default editor for files detected as binary. If undefined the user will be presented with a picker."), } } diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index d05ff6a8ab5..2fa3f3998d6 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/editordroptarget'; -import { LocalSelectionTransfer, DraggedEditorIdentifier, ResourcesDropHandler, DraggedEditorGroupIdentifier, containsDragType, CodeDataTransfers, DraggedTreeItemsIdentifier, extractTreeDropData } from 'vs/workbench/browser/dnd'; +import { Extensions as DragAndDropExtensions, LocalSelectionTransfer, DraggedEditorIdentifier, ResourcesDropHandler, DraggedEditorGroupIdentifier, containsDragType, CodeDataTransfers, DraggedTreeItemsIdentifier, extractTreeDropData, IDragAndDropContributionRegistry } from 'vs/workbench/browser/dnd'; import { addDisposableListener, EventType, EventHelper, isAncestor, DragAndDropObserver } from 'vs/base/browser/dom'; import { IEditorGroupsAccessor, IEditorGroupView, fillActiveEditorViewState } from 'vs/workbench/browser/parts/editor/editor'; import { EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; @@ -22,6 +22,7 @@ import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Registry } from 'vs/platform/registry/common/platform'; interface IDropOperation { splitDirection?: GroupDirection; @@ -575,10 +576,14 @@ export class EditorDropTarget extends Themable { if ( !this.editorTransfer.hasData(DraggedEditorIdentifier.prototype) && !this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype) && - event.dataTransfer && !containsDragType(event, DataTransfers.FILES, CodeDataTransfers.FILES, DataTransfers.RESOURCES, DataTransfers.TERMINALS, CodeDataTransfers.EDITORS) // see https://github.com/microsoft/vscode/issues/25789 + event.dataTransfer ) { - event.dataTransfer.dropEffect = 'none'; - return; // unsupported transfer + const dndContributions = Registry.as<IDragAndDropContributionRegistry>(DragAndDropExtensions.DragAndDropContribution).getAll(); + const dndContributionKeys = Array.from(dndContributions).map(e => e.dataFormatKey); + if (!containsDragType(event, DataTransfers.FILES, CodeDataTransfers.FILES, DataTransfers.RESOURCES, CodeDataTransfers.EDITORS, ...dndContributionKeys)) { // see https://github.com/microsoft/vscode/issues/25789 + event.dataTransfer.dropEffect = 'none'; + return; // unsupported transfer + } } // Signal DND start diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 2fdee0b8f17..0286fc0b08f 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -1135,7 +1135,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private async doHandleOpenEditorError(error: Error, editor: EditorInput, options?: IEditorOptions): Promise<void> { // Report error only if we are not told to ignore errors that occur from opening an editor - if (!isCancellationError(error) && (!options || !options.ignoreError)) { + if (!isCancellationError(error) && !options?.ignoreError) { // Always log the error to figure out what is going on this.logService.error(error); @@ -1710,9 +1710,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region closeEditors() - async closeEditors(args: EditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise<void> { + async closeEditors(args: EditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise<boolean> { if (this.isEmpty) { - return; + return true; } const editors = this.doGetEditorsToClose(args); @@ -1720,11 +1720,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Check for dirty and veto const veto = await this.handleDirtyClosing(editors.slice(0)); if (veto) { - return; + return false; } // Do close this.doCloseEditors(editors, options); + + return true; } private doGetEditorsToClose(args: EditorInput[] | ICloseEditorsFilter): EditorInput[] { diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index 4558ce5618e..a587913b667 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -117,27 +117,26 @@ export class EditorPanes extends Disposable { try { return await this.doOpenEditor(this.getEditorPaneDescriptor(editor), editor, options, context); } catch (error) { - if (!context.newInGroup) { - const isUnavailableResource = (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND; - const editorPlaceholder = isUnavailableResource ? UnavailableResourceErrorEditor.DESCRIPTOR : UnknownErrorEditor.DESCRIPTOR; - - // The editor is restored (as opposed to being newly opened) and as - // such we want to preserve the fact that an editor was opened here - // before by falling back to a editor placeholder that allows the - // user to retry the operation. - // - // This is especially important when an editor is dirty and fails to - // restore after a restart to prevent the impression that any user - // data is lost. - // - // Related: https://github.com/microsoft/vscode/issues/110062 - return { - ...(await this.doOpenEditor(editorPlaceholder, editor, options, context)), - error - }; - } - return { error }; + // We failed to open an editor and thus fallback to show a generic + // editor in error state as a way for the user to be aware. + // Previously we would immediately close the editor and show a + // error notification which was easy to not see. + // + // Besides, we want to preserve the users editor UI state as much + // as possible, so closing editors is never really an option. + // + // Related issues: + // - https://github.com/microsoft/vscode/issues/110062 + // - https://github.com/microsoft/vscode/issues/142875 + + const isUnavailableResource = (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND; + const editorPlaceholder = isUnavailableResource ? UnavailableResourceErrorEditor.DESCRIPTOR : UnknownErrorEditor.DESCRIPTOR; + + return { + ...(await this.doOpenEditor(editorPlaceholder, editor, options, context)), + error + }; } } diff --git a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index b8f058b1bb9..af524fb057f 100644 --- a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -20,7 +20,7 @@ import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceContextService, isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { EditorOpenSource, IEditorOptions } from 'vs/platform/editor/common/editor'; -import { EditorPaneDescriptor } from 'vs/workbench/browser/editor'; +import { computeEditorAriaLabel, EditorPaneDescriptor } from 'vs/workbench/browser/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Link } from 'vs/platform/opener/browser/link'; @@ -66,15 +66,18 @@ abstract class EditorPlaceholderPane extends EditorPane { } // Render Input - this.inputDisposable.value = this.renderInput(); + this.inputDisposable.value = this.renderInput(input); } - private renderInput(): IDisposable { + private renderInput(input: EditorInput): IDisposable { const [container, scrollbar] = assertAllDefined(this.container, this.scrollbar); // Reset any previous contents clearNode(container); + // Update ARIA label + container.setAttribute('aria-label', computeEditorAriaLabel(input, undefined, this.group, undefined)); + // Delegate to implementation const disposables = new DisposableStore(); this.renderBody(container, disposables); diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 3e6d43657dd..92091e6054d 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -1506,7 +1506,7 @@ export class ChangeEncodingAction extends Action { } const activeEncodingSupport = toEditorWithEncodingSupport(this.editorService.activeEditorPane.input); - if (typeof encoding.id !== 'undefined' && activeEncodingSupport && activeEncodingSupport.getEncoding() !== encoding.id) { + if (typeof encoding.id !== 'undefined' && activeEncodingSupport) { await activeEncodingSupport.setEncoding(encoding.id, isReopenWithEncoding ? EncodingMode.Decode : EncodingMode.Encode); // Set new encoding } diff --git a/src/vs/workbench/browser/parts/editor/editorsObserver.ts b/src/vs/workbench/browser/parts/editor/editorsObserver.ts index aae463254b5..96888700ba2 100644 --- a/src/vs/workbench/browser/parts/editor/editorsObserver.ts +++ b/src/vs/workbench/browser/parts/editor/editorsObserver.ts @@ -339,12 +339,28 @@ export class EditorsObserver extends Disposable { } private async doEnsureOpenedEditorsLimit(limit: number, mostRecentEditors: IEditorIdentifier[], exclude?: IEditorIdentifier): Promise<void> { - if (limit >= mostRecentEditors.length) { + + // Check for `excludeDirty` setting and apply it by excluding + // any recent editor that is dirty from the opened editors limit + let mostRecentEditorsCountingForLimit: IEditorIdentifier[]; + if (this.editorGroupsService.partOptions.limit?.excludeDirty) { + mostRecentEditorsCountingForLimit = mostRecentEditors.filter(({ editor }) => { + if (editor.isDirty() && !editor.isSaving()) { + return false; + } + + return true; + }); + } else { + mostRecentEditorsCountingForLimit = mostRecentEditors; + } + + if (limit >= mostRecentEditorsCountingForLimit.length) { return; // only if opened editors exceed setting and is valid and enabled } // Extract least recently used editors that can be closed - const leastRecentlyClosableEditors = mostRecentEditors.reverse().filter(({ editor, groupId }) => { + const leastRecentlyClosableEditors = mostRecentEditorsCountingForLimit.reverse().filter(({ editor, groupId }) => { if (editor.isDirty() && !editor.isSaving()) { return false; // not dirty editors (unless in the process of saving) } @@ -361,7 +377,7 @@ export class EditorsObserver extends Disposable { }); // Close editors until we reached the limit again - let editorsToCloseCount = mostRecentEditors.length - limit; + let editorsToCloseCount = mostRecentEditorsCountingForLimit.length - limit; const mapGroupToEditorsToClose = new Map<GroupIdentifier, EditorInput[]>(); for (const { groupId, editor } of leastRecentlyClosableEditors) { let editorsInGroupToClose = mapGroupToEditorsToClose.get(groupId); diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index c8c7c4a8df2..91df2786f62 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -10,7 +10,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { MenuId, IMenuService, registerAction2, Action2, IMenu } from 'vs/platform/actions/common/actions'; import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem, ITreeViewDragAndDropController } from 'vs/workbench/common/views'; +import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem, ITreeViewDragAndDropController, IViewBadge } from 'vs/workbench/common/views'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IThemeService, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -63,6 +63,7 @@ import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViews import { generateUuid } from 'vs/base/common/uuid'; 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'; export class TreeViewPane extends ViewPane { @@ -222,7 +223,8 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { @INotificationService private readonly notificationService: INotificationService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IHoverService private readonly hoverService: IHoverService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IActivityService private readonly activityService: IActivityService ) { super(); this.root = new Root(); @@ -349,6 +351,35 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { this._onDidChangeDescription.fire(this._description); } + private _badge: IViewBadge | undefined; + private _badgeActivity: IDisposable | undefined; + get badge(): IViewBadge | undefined { + return this._badge; + } + + set badge(badge: IViewBadge | undefined) { + + if (this._badge?.value === badge?.value && + this._badge?.tooltip === badge?.tooltip) { + return; + } + + if (this._badgeActivity) { + this._badgeActivity.dispose(); + this._badgeActivity = undefined; + } + + this._badge = badge; + + if (badge) { + const activity = { + badge: new NumberBadge(badge.value, () => badge.tooltip), + priority: 150 + }; + this._badgeActivity = this.activityService.showViewActivity(this.id, activity); + } + } + get canSelectMany(): boolean { return this._canSelectMany; } @@ -993,7 +1024,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS templateData.resourceLabel.setResource({ name: label, description, resource: labelResource }, { fileKind: this.getFileKind(node), title, - hideIcon: !!iconUrl || (!!node.themeIcon && !this.isFileKindThemeIcon(node.themeIcon)), + hideIcon: !!iconUrl || !!node.themeIcon, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData), @@ -1014,7 +1045,9 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS templateData.icon.style.backgroundImage = DOM.asCSSUrl(iconUrl); } else { let iconClass: string | undefined; - if (node.themeIcon && !this.isFileKindThemeIcon(node.themeIcon)) { + // If there is a resource for this tree item then we should respect the file icon theme's choice about + // whether to show a folder icon. + if (node.themeIcon && (!resource || !this.isFolderThemeIcon(node.themeIcon) || this.themeService.getFileIconTheme().hasFolderIcons)) { iconClass = ThemeIcon.asClassName(node.themeIcon); if (node.themeIcon.color) { templateData.icon.style.color = this.themeService.getColorTheme().getColor(node.themeIcon.color.id)?.toString() ?? ''; @@ -1047,9 +1080,13 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS container.parentElement!.classList.toggle('align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem)); } + private isFolderThemeIcon(icon: ThemeIcon | undefined): boolean { + return icon?.id === FolderThemeIcon.id; + } + private isFileKindThemeIcon(icon: ThemeIcon | undefined): boolean { if (icon) { - return icon.id === FileThemeIcon.id || icon.id === FolderThemeIcon.id; + return icon.id === FileThemeIcon.id || this.isFolderThemeIcon(icon); } else { return false; } @@ -1228,8 +1265,9 @@ export class CustomTreeView extends AbstractTreeView { @IContextKeyService contextKeyService: IContextKeyService, @IHoverService hoverService: IHoverService, @IExtensionService private readonly extensionService: IExtensionService, + @IActivityService activityService: IActivityService ) { - super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService); + super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService, activityService); } protected activate() { diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index cbcbac8602c..5e94ea2bba1 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -112,16 +112,20 @@ export class BrowserWindow extends Disposable { private create(): void { - // Driver - if (this.environmentService.options?.developmentOptions?.enableSmokeTestDriver) { - (async () => this._register(await registerWindowDriver()))(); - } - // Handle open calls this.setupOpenHandlers(); // Label formatting this.registerLabelFormatters(); + + // Smoke Test Driver + this.setupDriver(); + } + + private setupDriver(): void { + if (this.environmentService.options?.developmentOptions?.enableSmokeTestDriver) { + registerWindowDriver(); + } } private setupOpenHandlers(): void { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 5481eb93f3c..5d43bf45a05 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -258,6 +258,11 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con 'exclusiveMinimum': 0, 'markdownDescription': localize('limitEditorsMaximum', "Controls the maximum number of opened editors. Use the `#workbench.editor.limit.perEditorGroup#` setting to control this limit per editor group or across all groups.") }, + 'workbench.editor.limit.excludeDirty': { + 'type': 'boolean', + 'default': false, + 'description': localize('limitEditorsExcludeDirty', "Controls if the maximum number of opened editors should exclude dirty editors for counting towards the configured limit.") + }, 'workbench.editor.limit.perEditorGroup': { 'type': 'boolean', 'default': false, diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 235d5d71f34..bd610ebd12d 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -1007,6 +1007,7 @@ interface IEditorPartConfiguration { splitOnDragAndDrop?: boolean; limit?: { enabled?: boolean; + excludeDirty?: boolean; value?: number; perEditorGroup?: boolean; }; diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 4fe469247c9..7b05513ce94 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -118,6 +118,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel const lang = await this.languageDetectionService.detectLanguage(this.textEditorModelHandle); if (lang && !this.isDisposed()) { this.setLanguageIdInternal(lang); + const languageName = this.languageService.getLanguageName(lang); if (languageName) { this.accessibilityService.alert(localize('languageAutoDetected', "Language {0} was automatically detected and set as the language mode.", languageName)); diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 5707215da91..fb7dcc2c042 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -586,8 +586,8 @@ export const ACTIVITY_BAR_ACTIVE_BACKGROUND = registerColor('activityBar.activeB export const ACTIVITY_BAR_DRAG_AND_DROP_BORDER = registerColor('activityBar.dropBorder', { dark: ACTIVITY_BAR_FOREGROUND, light: ACTIVITY_BAR_FOREGROUND, - hcDark: ACTIVITY_BAR_FOREGROUND, - hcLight: ACTIVITY_BAR_FOREGROUND, + hcDark: null, + hcLight: null, }, localize('activityBarDragAndDropBorder', "Drag and drop feedback color for the activity bar items. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_BADGE_BACKGROUND = registerColor('activityBarBadge.background', { diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index e7793928e29..2fcb986f660 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -652,6 +652,8 @@ export interface ITreeView extends IDisposable { description: string | undefined; + badge: IViewBadge | undefined; + readonly visible: boolean; readonly onDidExpandItem: Event<ITreeItem>; @@ -855,3 +857,8 @@ export interface IViewPaneContainer { toggleViewVisibility(viewId: string): void; saveState(): void; } + +export interface IViewBadge { + readonly tooltip: string; + readonly value: number; +} diff --git a/src/vs/workbench/common/webview.ts b/src/vs/workbench/common/webview.ts index d551b2e12e4..aa598f1133d 100644 --- a/src/vs/workbench/common/webview.ts +++ b/src/vs/workbench/common/webview.ts @@ -18,7 +18,7 @@ export interface WebviewRemoteInfo { * This is hardcoded because we never expect to actually hit it. Instead these requests * should always go to a service worker. */ -export const webviewResourceBaseHost = 'vscode-webview.net'; +export const webviewResourceBaseHost = 'vscode-cdn.net'; export const webviewRootResourceAuthority = `vscode-resource.${webviewResourceBaseHost}`; @@ -31,7 +31,7 @@ export const webviewGenericCspSource = `https://*.${webviewResourceBaseHost}`; * we know where to load the resource from (remote or truly local): * * ```txt - * ${scheme}+${resource-authority}.vscode-resource.vscode-webview.net/${path} + * ${scheme}+${resource-authority}.vscode-resource.vscode-cdn.net/${path} * ``` * * @param resource Uri of the resource to load. diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts index 6b0bb5fdf18..602b81639e2 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts @@ -37,8 +37,8 @@ export class BulkCellEdits { @INotebookEditorModelResolverService private readonly _notebookModelService: INotebookEditorModelResolverService, ) { } - async apply(): Promise<void> { - + async apply(): Promise<readonly URI[]> { + const resources: URI[] = []; const editsByNotebook = groupBy(this._edits, (a, b) => compare(a.resource.toString(), b.resource.toString())); for (let group of editsByNotebook) { @@ -56,10 +56,14 @@ export class BulkCellEdits { // apply edits const edits = group.map(entry => entry.cellEdit); - ref.object.notebook.applyEdits(edits, true, undefined, () => undefined, this._undoRedoGroup); + ref.object.notebook.applyEdits(edits, true, undefined, () => undefined, this._undoRedoGroup, true); ref.dispose(); this._progress.report(undefined); + + resources.push(first.resource); } + + return resources; } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index a259de1b6d5..aaab23b4da9 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -21,7 +21,12 @@ import { LinkedList } from 'vs/base/common/linkedList'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { ResourceMap } from 'vs/base/common/map'; +import { ResourceMap, ResourceSet } from 'vs/base/common/map'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { URI } from 'vs/base/common/uri'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; class BulkEdit { @@ -67,10 +72,10 @@ class BulkEdit { } } - async perform(): Promise<void> { + async perform(): Promise<readonly URI[]> { if (this._edits.length === 0) { - return; + return []; } const ranges: number[] = [1]; @@ -88,6 +93,7 @@ class BulkEdit { // Increment by percentage points since progress API expects that const progress: IProgress<void> = { report: _ => this._progress.report({ increment: 100 / this._edits.length }) }; + const resources: (readonly URI[])[] = []; let index = 0; for (let range of ranges) { if (this._token.isCancellationRequested) { @@ -95,34 +101,36 @@ class BulkEdit { } const group = this._edits.slice(index, index + range); if (group[0] instanceof ResourceFileEdit) { - await this._performFileEdits(<ResourceFileEdit[]>group, this._undoRedoGroup, this._undoRedoSource, this._confirmBeforeUndo, progress); + resources.push(await this._performFileEdits(<ResourceFileEdit[]>group, this._undoRedoGroup, this._undoRedoSource, this._confirmBeforeUndo, progress)); } else if (group[0] instanceof ResourceTextEdit) { - await this._performTextEdits(<ResourceTextEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress); + resources.push(await this._performTextEdits(<ResourceTextEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress)); } else if (group[0] instanceof ResourceNotebookCellEdit) { - await this._performCellEdits(<ResourceNotebookCellEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress); + resources.push(await this._performCellEdits(<ResourceNotebookCellEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress)); } else { console.log('UNKNOWN EDIT'); } index = index + range; } + + return resources.flat(); } - private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, confirmBeforeUndo: boolean, progress: IProgress<void>) { + private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, confirmBeforeUndo: boolean, progress: IProgress<void>): Promise<readonly URI[]> { this._logService.debug('_performFileEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._code || 'undoredo.workspaceEdit', undoRedoGroup, undoRedoSource, confirmBeforeUndo, progress, this._token, edits); - await model.apply(); + return await model.apply(); } - private async _performTextEdits(edits: ResourceTextEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<void> { + private async _performTextEdits(edits: ResourceTextEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<readonly URI[]> { this._logService.debug('_performTextEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._code || 'undoredo.workspaceEdit', this._editor, undoRedoGroup, undoRedoSource, progress, this._token, edits); - await model.apply(); + return await model.apply(); } - private async _performCellEdits(edits: ResourceNotebookCellEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<void> { + private async _performCellEdits(edits: ResourceNotebookCellEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<readonly URI[]> { this._logService.debug('_performCellEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkCellEdits, undoRedoGroup, undoRedoSource, progress, this._token, edits); - await model.apply(); + return await model.apply(); } } @@ -138,7 +146,9 @@ export class BulkEditService implements IBulkEditService { @ILogService private readonly _logService: ILogService, @IEditorService private readonly _editorService: IEditorService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, - @IDialogService private readonly _dialogService: IDialogService + @IDialogService private readonly _dialogService: IDialogService, + @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, + @IConfigurationService private readonly _configService: IConfigurationService, ) { } setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable { @@ -212,8 +222,15 @@ export class BulkEditService implements IBulkEditService { let listener: IDisposable | undefined; try { - listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this.shouldVeto(label, e.reason), 'veto.blukEditService')); - await bulkEdit.perform(); + listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this._shouldVeto(label, e.reason), 'veto.blukEditService')); + const resources = await bulkEdit.perform(); + + // when enabled (option AND setting) loop over all dirty working copies and trigger save + // for those that were involved in this bulk edit operation. + if (options?.respectAutoSaveConfig && this._configService.getValue(autoSaveSetting) === true && resources.length > 1) { + await this._saveAll(resources); + } + return { ariaSummary: bulkEdit.ariaMessage() }; } catch (err) { // console.log('apply FAILED'); @@ -226,7 +243,23 @@ export class BulkEditService implements IBulkEditService { } } - private async shouldVeto(label: string | undefined, reason: ShutdownReason): Promise<boolean> { + private async _saveAll(resources: readonly URI[]) { + const set = new ResourceSet(resources); + const saves = this._workingCopyService.dirtyWorkingCopies.map(async (copy) => { + if (set.has(copy.resource)) { + await copy.save(); + } + }); + + const result = await Promise.allSettled(saves); + for (const item of result) { + if (item.status === 'rejected') { + this._logService.warn(item.reason); + } + } + } + + private async _shouldVeto(label: string | undefined, reason: ShutdownReason): Promise<boolean> { label = label || localize('fileOperation', "File operation"); const reasonLabel = reason === ShutdownReason.CLOSE ? localize('closeTheWindow', "Close Window") : reason === ShutdownReason.LOAD ? localize('changeWorkspace', "Change Workspace") : reason === ShutdownReason.RELOAD ? localize('reloadTheWindow', "Reload Window") : localize('quit', "Quit"); @@ -240,3 +273,16 @@ export class BulkEditService implements IBulkEditService { } registerSingleton(IBulkEditService, BulkEditService, true); + +const autoSaveSetting = 'files.refactoring.autoSave'; + +Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({ + id: 'files', + properties: { + [autoSaveSetting]: { + description: localize('refactoring.autoSave', "Controls if files that were part of a refactoring are saved automatically"), + default: true, + type: 'boolean' + } + } +}); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index b3247bba89d..86de76556d2 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -16,7 +16,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { VSBuffer } from 'vs/base/common/buffer'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { flatten, tail } from 'vs/base/common/arrays'; +import { tail } from 'vs/base/common/arrays'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; interface IFileOperation { @@ -51,7 +51,7 @@ class RenameOperation implements IFileOperation { ) { } get uris() { - return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri])); + return this._edits.map(edit => [edit.newUri, edit.oldUri]).flat(); } async perform(token: CancellationToken): Promise<IFileOperation> { @@ -105,7 +105,7 @@ class CopyOperation implements IFileOperation { ) { } get uris() { - return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri])); + return this._edits.map(edit => [edit.newUri, edit.oldUri]).flat(); } async perform(token: CancellationToken): Promise<IFileOperation> { @@ -293,7 +293,7 @@ class FileUndoRedoElement implements IWorkspaceUndoRedoElement { readonly operations: IFileOperation[], readonly confirmBeforeUndo: boolean ) { - this.resources = (<URI[]>[]).concat(...operations.map(op => op.uris)); + this.resources = operations.map(op => op.uris).flat(); } async undo(): Promise<void> { @@ -332,7 +332,7 @@ export class BulkFileEdits { @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, ) { } - async apply(): Promise<void> { + async apply(): Promise<readonly URI[]> { const undoOperations: IFileOperation[] = []; const undoRedoInfo = { undoRedoGroupId: this._undoRedoGroup.id }; @@ -350,7 +350,7 @@ export class BulkFileEdits { } if (edits.length === 0) { - return; + return []; } const groups: Array<RenameEdit | CopyEdit | DeleteEdit | CreateEdit>[] = []; @@ -395,6 +395,8 @@ export class BulkFileEdits { this._progress.report(undefined); } - this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, this._code, undoOperations, this._confirmBeforeUndo), this._undoRedoGroup, this._undoRedoSource); + const undoRedoElement = new FileUndoRedoElement(this._label, this._code, undoOperations, this._confirmBeforeUndo); + this._undoRedoService.pushElement(undoRedoElement, this._undoRedoGroup, this._undoRedoSource); + return undoRedoElement.resources; } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index b0f3ba726bb..b1ff888c894 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -232,16 +232,17 @@ export class BulkTextEdits { return { canApply: true }; } - async apply(): Promise<void> { + async apply(): Promise<readonly URI[]> { this._validateBeforePrepare(); const tasks = await this._createEditsTasks(); - if (this._token.isCancellationRequested) { - return; - } try { + if (this._token.isCancellationRequested) { + return []; + } + const resources: URI[] = []; const validation = this._validateTasks(tasks); if (!validation.canApply) { throw new Error(`${validation.reason.toString()} has changed in the meantime`); @@ -254,6 +255,7 @@ export class BulkTextEdits { this._undoRedoService.pushElement(singleModelEditStackElement, this._undoRedoGroup, this._undoRedoSource); task.apply(); singleModelEditStackElement.close(); + resources.push(task.model.uri); } this._progress.report(undefined); } else { @@ -267,10 +269,13 @@ export class BulkTextEdits { for (const task of tasks) { task.apply(); this._progress.report(undefined); + resources.push(task.model.uri); } multiModelEditStackElement.close(); } + return resources; + } finally { dispose(tasks); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts index db38d82e544..06f90fcfd98 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts @@ -6,6 +6,7 @@ import './menuPreventer'; import './accessibility/accessibility'; import './diffEditorHelper'; +import './editorSettingsMigration'; import './inspectKeybindings'; import './largeFileOptimizations'; import './inspectEditorTokens/inspectEditorTokens'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/editorSettingsMigration.ts b/src/vs/workbench/contrib/codeEditor/browser/editorSettingsMigration.ts new file mode 100644 index 00000000000..6fc9af98508 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/editorSettingsMigration.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { ConfigurationTarget, IConfigurationOverrides, IConfigurationService, IConfigurationValue } from 'vs/platform/configuration/common/configuration'; +import { EditorSettingMigration, ISettingsReader, ISettingsWriter } from 'vs/editor/browser/config/migrateOptions'; +import { Disposable } from 'vs/base/common/lifecycle'; + +class EditorSettingsMigration extends Disposable implements IWorkbenchContribution { + + constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + ) { + super(); + this._register(this._workspaceService.onDidChangeWorkspaceFolders(async (e) => { + for (const folder of e.added) { + await this._migrateEditorSettingsForFolder(folder); + } + })); + this._migrateEditorSettings(); + } + + private async _migrateEditorSettings(): Promise<void> { + await this._migrateEditorSettingsForFolder(undefined); + for (const folder of this._workspaceService.getWorkspace().folders) { + await this._migrateEditorSettingsForFolder(folder); + } + } + + private async _migrateEditorSettingsForFolder(folder: IWorkspaceFolder | undefined): Promise<void> { + await Promise.all(EditorSettingMigration.items.map(migration => this._migrateEditorSettingForFolderAndOverride(migration, { resource: folder?.uri }))); + } + + private async _migrateEditorSettingForFolderAndOverride(migration: EditorSettingMigration, overrides: IConfigurationOverrides): Promise<void> { + const data = this._configurationService.inspect(`editor.${migration.key}`, overrides); + + await this._migrateEditorSettingForFolderOverrideAndTarget(migration, overrides, data, 'userValue', ConfigurationTarget.USER); + await this._migrateEditorSettingForFolderOverrideAndTarget(migration, overrides, data, 'userLocalValue', ConfigurationTarget.USER_LOCAL); + await this._migrateEditorSettingForFolderOverrideAndTarget(migration, overrides, data, 'userRemoteValue', ConfigurationTarget.USER_REMOTE); + await this._migrateEditorSettingForFolderOverrideAndTarget(migration, overrides, data, 'workspaceFolderValue', ConfigurationTarget.WORKSPACE_FOLDER); + await this._migrateEditorSettingForFolderOverrideAndTarget(migration, overrides, data, 'workspaceValue', ConfigurationTarget.WORKSPACE); + + if (typeof overrides.overrideIdentifier === 'undefined' && typeof data.overrideIdentifiers !== 'undefined') { + for (const overrideIdentifier of data.overrideIdentifiers) { + await this._migrateEditorSettingForFolderAndOverride(migration, { resource: overrides.resource, overrideIdentifier }); + } + } + } + + private async _migrateEditorSettingForFolderOverrideAndTarget(migration: EditorSettingMigration, overrides: IConfigurationOverrides, data: IConfigurationValue<any>, dataKey: keyof IConfigurationValue<any>, target: ConfigurationTarget): Promise<void> { + const value = data[dataKey]; + if (typeof value === 'undefined') { + return; + } + + const writeCalls: [string, any][] = []; + const read: ISettingsReader = (key: string) => this._configurationService.inspect(`editor.${key}`, overrides)[dataKey]; + const write: ISettingsWriter = (key: string, value: any) => writeCalls.push([key, value]); + migration.migrate(value, read, write); + for (const [wKey, wValue] of writeCalls) { + await this._configurationService.updateValue(`editor.${wKey}`, wValue, overrides, target); + } + } +} + +Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorSettingsMigration, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index 7b54eef8f20..1e220e2de14 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -15,7 +15,7 @@ import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBo import { SimpleButton, findPreviousMatchIcon, findNextMatchIcon } from 'vs/editor/contrib/find/browser/findWidget'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground, errorForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ContextScopedFindInput } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { widgetClose } from 'vs/platform/theme/common/iconRegistry'; @@ -26,6 +26,12 @@ const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', " const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next Match"); const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close"); +interface IFindOptions { + showOptionButtons?: boolean; + checkImeCompletionState?: boolean; + showResultCount?: boolean; +} + export abstract class SimpleFindWidget extends Widget { private readonly _findInput: FindInput; private readonly _domNode: HTMLElement; @@ -35,16 +41,16 @@ export abstract class SimpleFindWidget extends Widget { private readonly _updateHistoryDelayer: Delayer<void>; private readonly prevBtn: SimpleButton; private readonly nextBtn: SimpleButton; + private _matchesCount: HTMLElement | undefined; private _isVisible: boolean = false; - private foundMatch: boolean = false; + private _foundMatch: boolean = false; constructor( @IContextViewService private readonly _contextViewService: IContextViewService, @IContextKeyService contextKeyService: IContextKeyService, private readonly _state: FindReplaceState = new FindReplaceState(), - showOptionButtons?: boolean, - checkImeCompletionState?: boolean + private readonly _options: IFindOptions ) { super(); @@ -59,20 +65,23 @@ export abstract class SimpleFindWidget extends Widget { new RegExp(value); return null; } catch (e) { - this.foundMatch = false; - this.updateButtons(this.foundMatch); + this._foundMatch = false; + this.updateButtons(this._foundMatch); return { content: e.message }; } } - }, contextKeyService, showOptionButtons)); + }, contextKeyService, _options.showOptionButtons)); // Find History with update delayer this._updateHistoryDelayer = new Delayer<void>(500); - this._register(this._findInput.onInput((e) => { - if (!checkImeCompletionState || !this._findInput.isImeSessionInProgress) { - this.foundMatch = this._onInputChanged(); - this.updateButtons(this.foundMatch); + this._register(this._findInput.onInput(async (e) => { + if (!_options.checkImeCompletionState || !this._findInput.isImeSessionInProgress) { + this._foundMatch = this._onInputChanged(); + if (this._options.showResultCount) { + await this.updateResultCount(); + } + this.updateButtons(this._foundMatch); this.focusFindBox(); this._delayedUpdateHistory(); } @@ -152,6 +161,14 @@ export abstract class SimpleFindWidget extends Widget { this._register(dom.addDisposableListener(this._innerDomNode, 'click', (event) => { event.stopPropagation(); })); + + if (_options?.showResultCount) { + this._domNode.classList.add('result-count'); + this._register(this._findInput.onDidChange(() => { + this.updateResultCount(); + this.updateButtons(this._foundMatch); + })); + } } protected abstract _onInputChanged(): boolean; @@ -161,6 +178,7 @@ export abstract class SimpleFindWidget extends Widget { protected abstract _onFocusTrackerBlur(): void; protected abstract _onFindInputFocusTrackerFocus(): void; protected abstract _onFindInputFocusTrackerBlur(): void; + protected abstract _getResultCount(): Promise<{ resultIndex: number; resultCount: number } | undefined>; protected get inputValue() { return this._findInput.getValue(); @@ -214,7 +232,7 @@ export abstract class SimpleFindWidget extends Widget { } this._isVisible = true; - this.updateButtons(this.foundMatch); + this.updateButtons(this._foundMatch); setTimeout(() => { this._innerDomNode.classList.add('visible', 'visible-transition'); @@ -243,7 +261,7 @@ export abstract class SimpleFindWidget extends Widget { // Need to delay toggling visibility until after Transition, then visibility hidden - removes from tabIndex list setTimeout(() => { this._isVisible = false; - this.updateButtons(this.foundMatch); + this.updateButtons(this._foundMatch); this._innerDomNode.classList.remove('visible'); }, 200); } @@ -281,6 +299,20 @@ export abstract class SimpleFindWidget extends Widget { this.nextBtn.focus(); this._findInput.inputBox.focus(); } + + async updateResultCount(): Promise<void> { + const count = await this._getResultCount(); + if (!this._matchesCount) { + this._matchesCount = document.createElement('div'); + this._matchesCount.className = 'matchesCount'; + } + this._matchesCount.innerText = ''; + const label = count === undefined || count.resultCount === 0 ? `No Results` : `${count.resultIndex + 1} of ${count.resultCount}`; + this._matchesCount.appendChild(document.createTextNode(label)); + this._matchesCount.classList.toggle('no-results', !count || count.resultCount === 0); + this._findInput?.domNode.insertAdjacentElement('afterend', this._matchesCount); + this._foundMatch = !!count && count.resultCount > 0; + } } // theming @@ -299,4 +331,9 @@ registerThemingParticipant((theme, collector) => { if (widgetShadowColor) { collector.addRule(`.monaco-workbench .simple-find-part { box-shadow: 0 0 8px 2px ${widgetShadowColor}; }`); } + + const error = theme.getColor(errorForeground); + if (error) { + collector.addRule(`.no-results.matchesCount { color: ${error}; }`); + } }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts index a3162f0ffa2..53dc09e2826 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts @@ -9,7 +9,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { CharacterPair, CommentRule, EnterAction, ExplicitLanguageConfiguration, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentAction, IndentationRule, OnEnterRule } from 'vs/editor/common/languages/languageConfiguration'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -89,7 +89,8 @@ export class LanguageConfigurationFileHandler extends Disposable { @ITextMateService textMateService: ITextMateService, @ILanguageService private readonly _languageService: ILanguageService, @IExtensionResourceLoaderService private readonly _extensionResourceLoaderService: IExtensionResourceLoaderService, - @IExtensionService private readonly _extensionService: IExtensionService + @IExtensionService private readonly _extensionService: IExtensionService, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, ) { super(); @@ -414,7 +415,7 @@ export class LanguageConfigurationFileHandler extends Disposable { __electricCharacterSupport: undefined, }; - LanguageConfigurationRegistry.register(languageId, richEditConfig, 50); + this._languageConfigurationService.register(languageId, richEditConfig, 50); } private _parseRegex(languageId: string, confPath: string, value: string | IRegExp): RegExp | undefined { diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index fbd43cd3aed..d99f9c41a0b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -45,6 +45,7 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { private _domNode: HTMLElement; private _body: HTMLElement; private _md: HTMLElement | undefined; + private _plainText: HTMLElement | undefined; private _clearTimeout: any; private _editAction: Action | null = null; @@ -125,8 +126,10 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { private updateCommentBody(body: string | IMarkdownString) { this._body.innerText = ''; this._md = undefined; + this._plainText = undefined; if (typeof body === 'string') { - this._body.innerText = body; + this._plainText = dom.append(this._body, dom.$('.comment-body-plainstring')); + this._plainText.innerText = body; } else { this._md = this.markdownRenderer.render(body).element; this._body.appendChild(this._md); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index ac743f0f7bb..b4989b77691 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -311,7 +311,6 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends content.push(`.review-widget .body code { font-family: var(${fontFamilyVar}); font-weight: var(${fontWeightVar}); - font-size: var(${fontSizeVar}); }`); this._styleElement.textContent = content.join('\n'); diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index 791a2b70e7b..2539c3e3960 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -108,6 +108,10 @@ padding: 0 2px 0 2px; } +.review-widget .body .review-comment .review-comment-contents .comment-body .comment-body-plainstring { + white-space: pre; +} + .review-widget .body .review-comment .review-comment-contents .comment-body { padding-top: 4px; } diff --git a/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts b/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts index 12411801f21..5ec8c29bae1 100644 --- a/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts +++ b/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts @@ -17,15 +17,15 @@ import { IStatusbarService } from 'vs/workbench/services/statusbar/browser/statu export const STATUS_BAR_DEBUGGING_BACKGROUND = registerColor('statusBar.debuggingBackground', { dark: '#CC6633', light: '#CC6633', - hcDark: '#CC6633', - hcLight: '#CC6633' + hcDark: '#BA592C', + hcLight: '#B5200D' }, localize('statusBarDebuggingBackground', "Status bar background color when a program is being debugged. The status bar is shown in the bottom of the window")); export const STATUS_BAR_DEBUGGING_FOREGROUND = registerColor('statusBar.debuggingForeground', { dark: STATUS_BAR_FOREGROUND, light: STATUS_BAR_FOREGROUND, hcDark: STATUS_BAR_FOREGROUND, - hcLight: STATUS_BAR_FOREGROUND + hcLight: '#FFFFFF' }, localize('statusBarDebuggingForeground', "Status bar foreground color when a program is being debugged. The status bar is shown in the bottom of the window")); export const STATUS_BAR_DEBUGGING_BORDER = registerColor('statusBar.debuggingBorder', { diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index d25efaffb6e..c27c88f27a6 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -6,6 +6,7 @@ import * as cp from 'child_process'; import { getDriveLetter } from 'vs/base/common/extpath'; import * as platform from 'vs/base/common/platform'; +// import { IProcessTreeNode } from 'windows-process-tree'; function spawnAsPromised(command: string, args: string[]): Promise<string> { return new Promise((resolve, reject) => { @@ -32,8 +33,8 @@ export async function hasChildProcesses(processId: number | undefined): Promise< if (platform.isWindows) { const windowsProcessTree = await import('windows-process-tree'); return new Promise<boolean>(resolve => { - windowsProcessTree.getProcessTree(processId, (processTree) => { - resolve(processTree.children.length > 0); + windowsProcessTree.getProcessTree(processId, processTree => { + resolve(!!processTree && processTree.children.length > 0); }); }); } else { diff --git a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts index f8735e39e90..fdcf9b5c181 100644 --- a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts +++ b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts @@ -7,7 +7,7 @@ import { IGrammarContributions, EmmetEditorAction } from 'vs/workbench/contrib/e import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; +import { ILanguageService } from 'vs/editor/common/languages/language'; class MockGrammarContributions implements IGrammarContributions { private scopeName: string; @@ -24,13 +24,14 @@ class MockGrammarContributions implements IGrammarContributions { suite('Emmet', () => { test('Get language mode and parent mode for emmet', () => { - withTestCodeEditor([], {}, (editor) => { + withTestCodeEditor([], {}, (editor, viewModel, instantiationService) => { + const languageService = instantiationService.get(ILanguageService); const disposables = new DisposableStore(); - disposables.add(ModesRegistry.registerLanguage({ id: 'markdown' })); - disposables.add(ModesRegistry.registerLanguage({ id: 'handlebars' })); - disposables.add(ModesRegistry.registerLanguage({ id: 'nunjucks' })); - disposables.add(ModesRegistry.registerLanguage({ id: 'laravel-blade' })); + disposables.add(languageService.registerLanguage({ id: 'markdown' })); + disposables.add(languageService.registerLanguage({ id: 'handlebars' })); + disposables.add(languageService.registerLanguage({ id: 'nunjucks' })); + disposables.add(languageService.registerLanguage({ id: 'laravel-blade' })); function testIsEnabled(mode: string, scopeName: string, expectedLanguage?: string, expectedParentLanguage?: string) { const model = editor.getModel(); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 0d0c23cdc12..41515d1c2e2 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -74,6 +74,8 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { Event } from 'vs/base/common/event'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { UnsupportedExtensionsMigrationContrib } from 'vs/workbench/contrib/extensions/browser/unsupportedExtensionsMigrationContribution'; +import { isWeb } from 'vs/base/common/platform'; +import { ExtensionsCleaner } from 'vs/workbench/contrib/extensions/browser/extensionsCleaner'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -1557,6 +1559,10 @@ workbenchRegistry.registerWorkbenchContribution(ExtensionDependencyChecker, Life workbenchRegistry.registerWorkbenchContribution(ExtensionEnablementWorkspaceTrustTransitionParticipant, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(ExtensionsCompletionItemsProvider, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(UnsupportedExtensionsMigrationContrib, LifecyclePhase.Eventually); +if (isWeb) { + workbenchRegistry.registerWorkbenchContribution(ExtensionsCleaner, LifecyclePhase.Eventually); +} + // Running Extensions registerAction2(ShowRuntimeExtensionsAction); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsCleaner.ts b/src/vs/workbench/contrib/extensions/browser/extensionsCleaner.ts new file mode 100644 index 00000000000..2d9b7872491 --- /dev/null +++ b/src/vs/workbench/contrib/extensions/browser/extensionsCleaner.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; + +export class ExtensionsCleaner implements IWorkbenchContribution { + + constructor( + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IStorageService storageService: IStorageService, + ) { + ExtensionStorageService.removeOutdatedExtensionVersions(extensionManagementService, storageService); + } +} diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 1ee398a691a..f7eca9c59df 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -7,10 +7,10 @@ import { localize } from 'vs/nls'; import { assertIsDefined } from 'vs/base/common/types'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { toAction } from 'vs/base/common/actions'; -import { VIEWLET_ID, TEXT_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; +import { VIEWLET_ID, TEXT_FILE_EDITOR_ID, BINARY_TEXT_FILE_MODE } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { IEditorOpenContext, EditorInputCapabilities, isTextEditorViewState } from 'vs/workbench/common/editor'; +import { IEditorOpenContext, EditorInputCapabilities, isTextEditorViewState, DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; @@ -24,7 +24,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IErrorWithActions } from 'vs/base/common/errorMessage'; import { EditorActivation, ITextEditorOptions } from 'vs/platform/editor/common/editor'; @@ -212,8 +212,9 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { private openAsBinary(input: FileEditorInput, options: ITextEditorOptions | undefined): void { const defaultBinaryEditor = this.configurationService.getValue<string | undefined>('workbench.editor.defaultBinaryEditor'); - const groupToOpen = this.group ?? this.editorGroupService.activeGroup; - const editorOptions = { + const group = this.group ?? this.editorGroupService.activeGroup; + + let editorOptions = { ...options, // Make sure to not steal away the currently active group // because we are triggering another openEditor() call @@ -222,20 +223,43 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { activation: EditorActivation.PRESERVE }; - // If we the user setting specifies a default binary editor we use that - if (defaultBinaryEditor && defaultBinaryEditor !== '') { - this.editorService.replaceEditors([{ - editor: input, - replacement: { resource: input.resource, options: { ...editorOptions, override: defaultBinaryEditor } } - }], groupToOpen); + // Check configuration and determine whether we open the binary + // file input in a different editor or going through the same + // editor. + // Going through the same editor is debt, and a better solution + // would be to introduce a real editor for the binary case + // and avoid enforcing binary or text on the file editor input. + + if (defaultBinaryEditor && defaultBinaryEditor !== '' && defaultBinaryEditor !== DEFAULT_EDITOR_ASSOCIATION.id) { + this.doOpenAsBinaryInDifferentEditor(group, defaultBinaryEditor, input, editorOptions); + } else { + this.doOpenAsBinaryInSameEditor(group, defaultBinaryEditor, input, editorOptions); } + } - // Otherwise we mark file input for forced binary opening and reopen the file - else { - input.setForceOpenAsBinary(); + private doOpenAsBinaryInDifferentEditor(group: IEditorGroup, editorId: string | undefined, editor: FileEditorInput, editorOptions: ITextEditorOptions): void { + this.editorService.replaceEditors([{ + editor, + replacement: { resource: editor.resource, options: { ...editorOptions, override: editorId } } + }], group); + } + + private doOpenAsBinaryInSameEditor(group: IEditorGroup, editorId: string | undefined, editor: FileEditorInput, editorOptions: ITextEditorOptions): void { - groupToOpen.openEditor(input, editorOptions); + // Open binary as text + if (editorId === DEFAULT_EDITOR_ASSOCIATION.id) { + editor.setForceOpenAsText(); + editor.setPreferredLanguageId(BINARY_TEXT_FILE_MODE); // https://github.com/microsoft/vscode/issues/131076 + + editorOptions = { ...editorOptions, forceReload: true }; // Same pane and same input, must force reload to clear cached state } + + // Open as binary + else { + editor.setForceOpenAsBinary(); + } + + group.openEditor(editor, editorOptions); } private async openAsFolder(input: FileEditorInput): Promise<void> { diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 317f7f2da0d..1a6f2c8f672 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -293,6 +293,11 @@ configurationRegistry.registerConfiguration({ 'type': 'boolean', 'description': nls.localize('files.simpleDialog.enable', "Enables the simple file dialog. The simple file dialog replaces the system file dialog when enabled."), 'default': false + }, + 'files.participants.timeout': { + type: 'number', + default: 60000, + markdownDescription: nls.localize('files.participants.timeout', "Timeout in milliseconds after which file participants for create, rename, and delete are cancelled. Use `0` to disable participants."), } } }); diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 55fb3414ea4..e34b213a567 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -14,7 +14,7 @@ import { EncodingMode, TextFileOperationError, TextFileOperationResult } from 'v import { FileOperationResult, FileOperationError, NotModifiedSinceFileOperationError, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { timeout } from 'vs/base/common/async'; -import { ModesRegistry, PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; @@ -171,7 +171,7 @@ suite('Files - FileEditorInput', () => { test('preferred language', async function () { const languageId = 'file-input-test'; - ModesRegistry.registerLanguage({ + const registration = accessor.languageService.registerLanguage({ id: languageId, }); @@ -190,6 +190,8 @@ suite('Files - FileEditorInput', () => { const model2 = await input2.resolve() as TextFileEditorModel; assert.strictEqual(model2.textEditorModel!.getLanguageId(), languageId); + + registration.dispose(); }); test('preferred contents', async function () { diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index 99d8c6897b6..52fe78d35bb 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -19,7 +19,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { editorBackground, editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPaneSelectionChangeReason, IEditorMemento, IEditorOpenContext, IEditorPaneSelectionChangeEvent } from 'vs/workbench/common/editor'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { InteractiveEditorInput } from 'vs/workbench/contrib/interactive/browser/interactiveEditorInput'; import { CodeCellLayoutChangeEvent, IActiveNotebookEditorDelegate, ICellViewModel, INotebookEditorViewState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -54,9 +54,10 @@ import { ModesHoverController } from 'vs/editor/contrib/hover/browser/hover'; import { MarkerController } from 'vs/editor/contrib/gotoError/browser/gotoError'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; -import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { ITextEditorOptions, TextEditorSelectionSource } from 'vs/platform/editor/common/editor'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { NOTEBOOK_KERNEL } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; const DECORATION_KEY = 'interactiveInputDecoration'; const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; @@ -110,6 +111,8 @@ export class InteractiveEditor extends EditorPane { #onDidFocusWidget = this._register(new Emitter<void>()); override get onDidFocus(): Event<void> { return this.#onDidFocusWidget.event; } + #onDidChangeSelection = this._register(new Emitter<IEditorPaneSelectionChangeEvent>()); + readonly onDidChangeSelection = this.#onDidChangeSelection.event; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -436,6 +439,10 @@ export class InteractiveEditor extends EditorPane { } })); + this.#widgetDisposableStore.add(this.#codeEditorWidget.onDidChangeCursorPosition(e => this.#onDidChangeSelection.fire({ reason: this.#toEditorPaneSelectionChangeReason(e) }))); + this.#widgetDisposableStore.add(this.#codeEditorWidget.onDidChangeModelContent(() => this.#onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.EDIT }))); + + this.#widgetDisposableStore.add(this.#notebookKernelService.onDidChangeNotebookAffinity(this.#syncWithKernel, this)); this.#widgetDisposableStore.add(this.#notebookKernelService.onDidChangeSelectedNotebooks(this.#syncWithKernel, this)); @@ -495,6 +502,15 @@ export class InteractiveEditor extends EditorPane { this.#syncWithKernel(); } + #toEditorPaneSelectionChangeReason(e: ICursorPositionChangedEvent): EditorPaneSelectionChangeReason { + switch (e.source) { + case TextEditorSelectionSource.PROGRAMMATIC: return EditorPaneSelectionChangeReason.PROGRAMMATIC; + case TextEditorSelectionSource.NAVIGATION: return EditorPaneSelectionChangeReason.NAVIGATION; + case TextEditorSelectionSource.JUMP: return EditorPaneSelectionChangeReason.JUMP; + default: return EditorPaneSelectionChangeReason.USER; + } + } + #lastCell: ICellViewModel | undefined = undefined; #lastCellDisposable = new DisposableStore(); #state: ScrollingState = ScrollingState.Initial; @@ -565,7 +581,11 @@ export class InteractiveEditor extends EditorPane { return; } - if (this.#lastCell instanceof CodeCellViewModel && (e as CodeCellLayoutChangeEvent).outputHeight === undefined && !this.#notebookWidget.value!.isScrolledToBottom()) { + if (!this.#notebookWidget.value) { + return; + } + + if (this.#lastCell instanceof CodeCellViewModel && (e as CodeCellLayoutChangeEvent).outputHeight === undefined && !this.#notebookWidget.value.isScrolledToBottom()) { return; } diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index d79b71400e5..d6d4689d90e 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -41,6 +41,21 @@ class LanguageStatusViewModel { } } +class StoredCounter { + + constructor(@IStorageService private readonly _storageService: IStorageService, private readonly _key: string) { } + + get value() { + return this._storageService.getNumber(this._key, StorageScope.GLOBAL, 0); + } + + increment(): number { + const n = this.value + 1; + this._storageService.store(this._key, n, StorageScope.GLOBAL, StorageTarget.MACHINE); + return n; + } +} + class EditorStatusContribution implements IWorkbenchContribution { private static readonly _id = 'status.languageStatus'; @@ -48,6 +63,7 @@ class EditorStatusContribution implements IWorkbenchContribution { private static readonly _keyDedicatedItems = 'languageStatus.dedicated'; private readonly _disposables = new DisposableStore(); + private readonly _interactionCounter: StoredCounter; private _dedicated = new Set<string>(); @@ -65,6 +81,7 @@ class EditorStatusContribution implements IWorkbenchContribution { ) { _storageService.onDidChangeValue(this._handleStorageChange, this, this._disposables); this._restoreState(); + this._interactionCounter = new StoredCounter(_storageService, 'languageStatus.interactCount'); _languageStatusService.onDidChange(this._update, this, this._disposables); _editorService.onDidActiveEditorChange(this._update, this, this._disposables); @@ -181,6 +198,37 @@ class EditorStatusContribution implements IWorkbenchContribution { } else { this._combinedEntry.update(props); } + + // animate the status bar icon whenever language status changes, repeat animation + // when severity is warning or error, don't show animation when showing progress/busy + const userHasInteractedWithStatus = this._interactionCounter.value >= 3; + const node = document.querySelector('.monaco-workbench .statusbar DIV#status\\.languageStatus span.codicon'); + if (node instanceof HTMLElement) { + const _wiggle = 'wiggle'; + const _repeat = 'repeat'; + if (!isOneBusy) { + node.classList.toggle(_wiggle, showSeverity || !userHasInteractedWithStatus); + node.classList.toggle(_repeat, showSeverity); + this._renderDisposables.add(dom.addDisposableListener(node, 'animationend', _e => node.classList.remove(_wiggle, _repeat))); + } else { + node.classList.remove(_wiggle, _repeat); + } + } + + // track when the hover shows (this is automagic and DOM mutation spying is needed...) + // use that as signal that the user has interacted/learned language status items work + if (!userHasInteractedWithStatus) { + const hoverTarget = document.querySelector('.monaco-workbench .context-view'); + if (hoverTarget instanceof HTMLElement) { + const observer = new MutationObserver(() => { + if (document.contains(element)) { + this._interactionCounter.increment(); + observer.disconnect(); + } + }); + observer.observe(document.body, { childList: true, subtree: true }); + } + } } // dedicated status bar items are shows as-is in the status bar diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index 8021133a7a1..131695f460a 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -3,9 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .statusbar DIV#status\.languageStatus .codicon-circle-large-filled::before { - font-size: 12px; - line-height: 18px; + +@keyframes wiggle { + 0% { + transform: rotate(0) scale(1) + } + + 15%, + 45% { + transform: rotate(.04turn) scale(1.1) + } + + 30%, + 60% { + transform: rotate(-.04turn) scale(1.2) + } + + 100% { + transform: rotate(0) scale(1) + } +} + +.monaco-workbench.enable-motion .statusbar DIV#status\.languageStatus span.codicon.wiggle { + animation-duration: .8s; + animation-iteration-count: 1; + animation-name: wiggle; +} + +.monaco-workbench.enable-motion .statusbar DIV#status\.languageStatus span.codicon.wiggle.repeat { + animation-iteration-count: 3; } .monaco-workbench .hover-language-status { @@ -17,61 +43,61 @@ border-bottom: 1px solid var(--vscode-notifications-border); } -.monaco-workbench .hover-language-status > .severity { +.monaco-workbench .hover-language-status>.severity { padding-right: 8px; flex: 1; margin: auto; display: none; } -.monaco-workbench .hover-language-status > .severity.sev3 { +.monaco-workbench .hover-language-status>.severity.sev3 { color: var(--vscode-notificationsErrorIcon-foreground) } -.monaco-workbench .hover-language-status > .severity.sev2 { +.monaco-workbench .hover-language-status>.severity.sev2 { color: var(--vscode-notificationsInfoIcon-foreground) } -.monaco-workbench .hover-language-status > .severity.show { +.monaco-workbench .hover-language-status>.severity.show { display: inherit; } -.monaco-workbench .hover-language-status > .element { +.monaco-workbench .hover-language-status>.element { display: flex; justify-content: space-between; vertical-align: middle; flex-grow: 100; } -.monaco-workbench .hover-language-status > .element > .left > .detail:not(:empty)::before { +.monaco-workbench .hover-language-status>.element>.left>.detail:not(:empty)::before { /* allow-any-unicode-next-line */ content: '–'; padding: 0 4px; opacity: 0.6; } -.monaco-workbench .hover-language-status > .element > .left > .label:empty { +.monaco-workbench .hover-language-status>.element>.left>.label:empty { display: none; } -.monaco-workbench .hover-language-status > .element .left { +.monaco-workbench .hover-language-status>.element .left { margin: auto 0; } -.monaco-workbench .hover-language-status > .element .right { +.monaco-workbench .hover-language-status>.element .right { margin: auto 0; display: flex; } -.monaco-workbench .hover-language-status > .element .right:not(:empty) { +.monaco-workbench .hover-language-status>.element .right:not(:empty) { padding-left: 16px; } -.monaco-workbench .hover-language-status > .element .right .monaco-link { +.monaco-workbench .hover-language-status>.element .right .monaco-link { margin: auto 0; white-space: nowrap; } -.monaco-workbench .hover-language-status > .element .right .monaco-action-bar:not(:first-child) { +.monaco-workbench .hover-language-status>.element .right .monaco-action-bar:not(:first-child) { padding-left: 8px; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/contributedStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/contributedStatusBarItemController.ts index 66037ed429c..7e3796f5342 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/contributedStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/contributedStatusBarItemController.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { flatten } from 'vs/base/common/arrays'; import { disposableTimeout, Throttler } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -119,7 +118,7 @@ class CellStatusBarHelper extends Disposable { return; } - const items = flatten(itemLists.map(itemList => itemList.items)); + const items = itemLists.map(itemList => itemList.items).flat(); const newIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); this._currentItemLists.forEach(itemList => itemList.dispose && itemList.dispose()); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts index 90a05458d9e..10037c96943 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts @@ -131,7 +131,7 @@ export function runPasteCells(editor: INotebookEditor, activeCell: ICellViewMode kind: SelectionStateType.Index, focus: { start: newFocusIndex, end: newFocusIndex + 1 }, selections: [{ start: newFocusIndex, end: newFocusIndex + pasteCells.items.length }] - }), undefined); + }), undefined, true); } else { if (editor.getLength() !== 0) { return false; @@ -148,7 +148,7 @@ export function runPasteCells(editor: INotebookEditor, activeCell: ICellViewMode kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 1, end: pasteCells.items.length + 1 }] - }), undefined); + }), undefined, true); } return true; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts index 6b4c1aeb914..cb7ab0f8586 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts @@ -279,7 +279,7 @@ class ImplictKernelSelector implements IDisposable { case NotebookCellsChangeType.ChangeCellContent: case NotebookCellsChangeType.ModelChange: case NotebookCellsChangeType.Move: - case NotebookCellsChangeType.ChangeLanguage: + case NotebookCellsChangeType.ChangeCellLanguage: logService.trace('IMPLICIT kernel selection because of change event', event.kind); selectKernel(); break; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts index 99a308a9bd4..662b1929a2e 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts @@ -41,6 +41,7 @@ import { isEqual } from 'vs/base/common/resources'; const FIND_HIDE_TRANSITION = 'find-hide-transition'; const FIND_SHOW_TRANSITION = 'find-show-transition'; let MAX_MATCHES_COUNT_WIDTH = 69; +const PROGRESS_BAR_DELAY = 200; // show progress for at least 200ms export class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEditorContribution { static id: string = 'workbench.notebook.find'; @@ -76,9 +77,9 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote if (e.isSearching) { if (this._state.isSearching) { - this._progressBar.infinite().show(); + this._progressBar.infinite().show(PROGRESS_BAR_DELAY); } else { - this._progressBar.stop(); + this._progressBar.stop().hide(); } } @@ -88,7 +89,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote } const matches = this._findModel.findMatches; - this._replaceAllBtn.setEnabled(matches.find(match => match.modelMatchCount < match.matches.length) === null); + this._replaceAllBtn.setEnabled(matches.length > 0 && matches.find(match => match.modelMatchCount < match.matches.length) === undefined); if (e.filters) { this._findInput.updateFilterState((this._state.filters?.markupPreview ?? false) || (this._state.filters?.codeOutput ?? false)); @@ -148,7 +149,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote if (currentMatch.isModelMatch) { const match = currentMatch.match as FindMatch; - this._progressBar.infinite().show(); + this._progressBar.infinite().show(PROGRESS_BAR_DELAY); const replacePattern = this.replacePattern; const replaceString = replacePattern.buildReplaceString(match.matches, this._state.preserveCase); @@ -168,7 +169,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote return; } - this._progressBar.infinite().show(); + this._progressBar.infinite().show(PROGRESS_BAR_DELAY); const replacePattern = this.replacePattern; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts index 38c9042b175..9e7e660b6b0 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -34,6 +34,7 @@ import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdown import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { filterIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; +import { isSafari } from 'vs/base/common/platform'; const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); @@ -73,57 +74,73 @@ class NotebookFindFilterActionViewItem extends DropdownMenuActionViewItem { } private getActions(): IAction[] { - return [ - { - checked: this.filters.markupInput, - class: undefined, - enabled: !this.filters.markupPreview, - id: 'findInMarkdownInput', - label: NOTEBOOK_FIND_IN_MARKUP_INPUT, - run: async () => { - this.filters.markupInput = !this.filters.markupInput; - }, - tooltip: '', - dispose: () => null + const markdownInput: IAction = { + checked: this.filters.markupInput, + class: undefined, + enabled: !this.filters.markupPreview, + id: 'findInMarkdownInput', + label: NOTEBOOK_FIND_IN_MARKUP_INPUT, + run: async () => { + this.filters.markupInput = !this.filters.markupInput; }, - { - checked: this.filters.markupPreview, - class: undefined, - enabled: true, - id: 'findInMarkdownInput', - label: NOTEBOOK_FIND_IN_MARKUP_PREVIEW, - run: async () => { - this.filters.markupPreview = !this.filters.markupPreview; - }, - tooltip: '', - dispose: () => null + tooltip: '', + dispose: () => null + }; + + const markdownPreview: IAction = { + checked: this.filters.markupPreview, + class: undefined, + enabled: true, + id: 'findInMarkdownInput', + label: NOTEBOOK_FIND_IN_MARKUP_PREVIEW, + run: async () => { + this.filters.markupPreview = !this.filters.markupPreview; }, - new Separator(), - { - checked: this.filters.codeInput, - class: undefined, - enabled: true, - id: 'findInCodeInput', - label: NOTEBOOK_FIND_IN_CODE_INPUT, - run: async () => { - this.filters.codeInput = !this.filters.codeInput; - }, - tooltip: '', - dispose: () => null + tooltip: '', + dispose: () => null + }; + + const codeInput: IAction = { + checked: this.filters.codeInput, + class: undefined, + enabled: true, + id: 'findInCodeInput', + label: NOTEBOOK_FIND_IN_CODE_INPUT, + run: async () => { + this.filters.codeInput = !this.filters.codeInput; }, - { - checked: this.filters.codeOutput, - class: undefined, - enabled: true, - id: 'findInCodeOutput', - label: NOTEBOOK_FIND_IN_CODE_OUTPUT, - run: async () => { - this.filters.codeOutput = !this.filters.codeOutput; - }, - tooltip: '', - dispose: () => null + tooltip: '', + dispose: () => null + }; + + const codeOutput = { + checked: this.filters.codeOutput, + class: undefined, + enabled: true, + id: 'findInCodeOutput', + label: NOTEBOOK_FIND_IN_CODE_OUTPUT, + run: async () => { + this.filters.codeOutput = !this.filters.codeOutput; }, - ]; + tooltip: '', + dispose: () => null + }; + + if (isSafari) { + return [ + markdownInput, + codeInput + ]; + } else { + return [ + markdownInput, + markdownPreview, + new Separator(), + codeInput, + codeOutput, + ]; + } + } override updateChecked(): void { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts index 239aa8064cf..6d2e6f4785e 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts @@ -23,7 +23,6 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { registerEditorAction, EditorAction } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Progress } from 'vs/platform/progress/common/progress'; -import { flatten } from 'vs/base/common/arrays'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; // format notebook @@ -91,7 +90,7 @@ registerAction2(class extends Action2 { return []; })); - await bulkEditService.apply(/* edit */flatten(allCellEdits), { label: localize('label', "Format Notebook"), code: 'undoredo.formatNotebook', }); + await bulkEditService.apply(/* edit */allCellEdits.flat(), { label: localize('label', "Format Notebook"), code: 'undoredo.formatNotebook', }); } finally { disposable.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index 44ffbe69d1f..473ff42494f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -430,11 +430,12 @@ export class NotebookCellOutline extends Disposable implements IOutline<OutlineE // cap the amount of characters that we look at and use the following logic // - for MD prefer headings (each header is an entry) // - otherwise use the first none-empty line of the cell (MD or code) - let content = cell.getText().substring(0, 10_000); + let content = this._getCellFirstNonEmptyLine(cell); let hasHeader = false; if (isMarkdown) { - for (const token of marked.lexer(content, { gfm: true })) { + const fullContent = cell.getText().substring(0, 10_000); + for (const token of marked.lexer(fullContent, { gfm: true })) { if (token.type === 'heading') { hasHeader = true; entries.push(new OutlineEntry(entries.length, token.depth, cell, renderMarkdownAsPlaintext({ value: token.text }).trim(), false, false)); @@ -558,6 +559,19 @@ export class NotebookCellOutline extends Disposable implements IOutline<OutlineE } } + private _getCellFirstNonEmptyLine(cell: ICellViewModel) { + const textBuffer = cell.textBuffer; + for (let i = 0; i < textBuffer.getLineCount(); i++) { + const firstNonWhitespace = textBuffer.getLineFirstNonWhitespaceColumn(i + 1); + const lineLength = textBuffer.getLineLength(i + 1); + if (firstNonWhitespace < lineLength) { + return textBuffer.getLineContent(i + 1); + } + } + + return cell.getText().substring(0, 10_000); + } + get isEmpty(): boolean { return this._entries.length === 0; } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts index 8e6f97a2216..d0c6379a54b 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts @@ -10,7 +10,7 @@ import { isDocumentExcludePattern, TransientCellMetadata, TransientDocumentMetad import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, args): { +CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor): { viewType: string; displayName: string; options: { transientOutputs: boolean; transientCellMetadata: TransientCellMetadata; transientDocumentMetadata: TransientDocumentMetadata }; diff --git a/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts b/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts index 66f576aa3f4..f4209f52ad9 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts @@ -143,7 +143,7 @@ export function runDeleteAction(editor: IActiveNotebookEditor, cell: ICellViewMo return { kind: SelectionStateType.Index, focus: { start: 0, end: 0 }, selections: [{ start: 0, end: 0 }] }; } } - }, undefined); + }, undefined, true); } else { const focus = editor.getFocus(); const edits: ICellReplaceEdit[] = [{ @@ -169,14 +169,14 @@ export function runDeleteAction(editor: IActiveNotebookEditor, cell: ICellViewMo textModel.applyEdits(edits, true, { kind: SelectionStateType.Index, focus: editor.getFocus(), selections: editor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: newFocus, selections: finalSelections - }), undefined); + }), undefined, true); } else { // users decide to delete a cell out of current focus/selection const newFocus = focus.start > targetCellIndex ? { start: focus.start - 1, end: focus.end - 1 } : focus; textModel.applyEdits(edits, true, { kind: SelectionStateType.Index, focus: editor.getFocus(), selections: editor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: newFocus, selections: finalSelections - }), undefined); + }), undefined, true); } } } @@ -222,7 +222,8 @@ export async function moveCellRange(context: INotebookCellActionContext, directi selections: editor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: newFocus, selections: [finalSelection] }), - undefined + undefined, + true ); const focusRange = editor.getSelections()[0] ?? editor.getFocus(); editor.revealCellRangeInView(focusRange); @@ -250,7 +251,8 @@ export async function moveCellRange(context: INotebookCellActionContext, directi selections: editor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: newFocus, selections: [finalSelection] }), - undefined + undefined, + true ); const focusRange = editor.getSelections()[0] ?? editor.getFocus(); @@ -304,7 +306,8 @@ export async function copyCellRange(context: INotebookCellActionContext, directi selections: selections }, () => ({ kind: SelectionStateType.Index, focus: focus, selections: selections }), - undefined + undefined, + true ); } else { // insert down, move selections @@ -328,7 +331,8 @@ export async function copyCellRange(context: INotebookCellActionContext, directi selections: selections }, () => ({ kind: SelectionStateType.Index, focus: newFocus, selections: newSelections }), - undefined + undefined, + true ); const focusRange = editor.getSelections()[0] ?? editor.getFocus(); @@ -624,10 +628,10 @@ export function insertCell( const insertIndex = cell ? (direction === 'above' ? index : nextIndex) : index; - return insertCellAtIndex(viewModel, insertIndex, initialText, language, type, undefined, [], true); + return insertCellAtIndex(viewModel, insertIndex, initialText, language, type, undefined, [], true, true); } -export function insertCellAtIndex(viewModel: NotebookViewModel, index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, outputs: IOutputDto[], synchronous: boolean, pushUndoStop: boolean = true): CellViewModel { +export function insertCellAtIndex(viewModel: NotebookViewModel, index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, outputs: IOutputDto[], synchronous: boolean, pushUndoStop: boolean): CellViewModel { const endSelections: ISelectionState = { kind: SelectionStateType.Index, focus: { start: index, end: index + 1 }, selections: [{ start: index, end: index + 1 }] }; viewModel.notebookDocument.applyEdits([ { @@ -648,29 +652,3 @@ export function insertCellAtIndex(viewModel: NotebookViewModel, index: number, s ], synchronous, { kind: SelectionStateType.Index, focus: viewModel.getFocus(), selections: viewModel.getSelections() }, () => endSelections, undefined, pushUndoStop); return viewModel.cellAt(index)!; } - - -/** - * - * @param index - * @param length - * @param newIdx in an index scheme for the state of the tree after the current cell has been "removed" - * @param synchronous - * @param pushedToUndoStack - */ -export function moveCellToIdx(editor: IActiveNotebookEditor, index: number, length: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean = true): boolean { - const viewCell = editor.cellAt(index) as CellViewModel | undefined; - if (!viewCell) { - return false; - } - - editor.textModel.applyEdits([ - { - editType: CellEditType.Move, - index, - length, - newIdx - } - ], synchronous, { kind: SelectionStateType.Index, focus: editor.getFocus(), selections: editor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: { start: newIdx, end: newIdx + 1 }, selections: [{ start: newIdx, end: newIdx + 1 }] }), undefined); - return true; -} diff --git a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts index 602e14a3940..588675f6403 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts @@ -208,7 +208,7 @@ registerAction2(class ClearCellOutputsAction extends NotebookCellAction { return; } - editor.textModel.applyEdits([{ editType: CellEditType.Output, index, outputs: [] }], true, undefined, () => undefined, undefined); + editor.textModel.applyEdits([{ editType: CellEditType.Output, index, outputs: [] }], true, undefined, () => undefined, undefined, true); const runState = notebookExecutionStateService.getCellExecution(context.cell.uri)?.state; if (runState !== NotebookCellExecutionState.Executing) { @@ -220,7 +220,7 @@ registerAction2(class ClearCellOutputsAction extends NotebookCellAction { executionOrder: null, lastRunSuccess: null } - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); } } }); @@ -266,7 +266,7 @@ registerAction2(class ClearAllCellOutputsAction extends NotebookAction { editor.textModel.applyEdits( editor.textModel.cells.map((cell, index) => ({ editType: CellEditType.Output, index, outputs: [] - })), true, undefined, () => undefined, undefined); + })), true, undefined, () => undefined, undefined, true); const clearExecutionMetadataEdits = editor.textModel.cells.map((cell, index) => { const runState = notebookExecutionStateService.getCellExecution(cell.uri)?.state; @@ -285,7 +285,7 @@ registerAction2(class ClearAllCellOutputsAction extends NotebookAction { } }).filter(edit => !!edit) as ICellEditOperation[]; if (clearExecutionMetadataEdits.length) { - context.notebookEditor.textModel.applyEdits(clearExecutionMetadataEdits, true, undefined, () => undefined, undefined); + context.notebookEditor.textModel.applyEdits(clearExecutionMetadataEdits, true, undefined, () => undefined, undefined, true); } } }); @@ -451,7 +451,7 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction<ICellR const index = context.notebookEditor.textModel.cells.indexOf(context.cell.model); context.notebookEditor.textModel.applyEdits( [{ editType: CellEditType.CellLanguage, index, language: languageId }], - true, undefined, () => undefined, undefined + true, undefined, () => undefined, undefined, true ); } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 5e007b57a28..89c87eb81e7 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -517,7 +517,7 @@ abstract class AbstractElementRenderer extends Disposable { this.notebookEditor.textModel!.applyEdits([ { editType: CellEditType.Metadata, index, metadata: result } - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); } catch { } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts index eea2a5a0c8c..91e50defad3 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -11,7 +11,7 @@ import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { hash } from 'vs/base/common/hash'; import { toFormattedString } from 'vs/base/common/jsonFormatter'; -import { ICellOutput, IOutputDto, IOutputItemDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICellOutput, INotebookTextModel, IOutputDto, IOutputItemDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; import { URI } from 'vs/base/common/uri'; import { NotebookDiffEditorEventDispatcher, NotebookDiffViewEventType } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; @@ -117,7 +117,7 @@ export abstract class DiffElementViewModelBase extends Disposable { private _metadataEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; constructor( - readonly mainDocumentTextModel: NotebookTextModel, + readonly mainDocumentTextModel: INotebookTextModel, readonly original: DiffNestedCellViewModel | undefined, readonly modified: DiffNestedCellViewModel | undefined, readonly type: 'unchanged' | 'insert' | 'delete' | 'modified', @@ -645,7 +645,7 @@ function outputsEqual(original: ICellOutput[], modified: ICellOutput[]) { return OutputComparison.Unchanged; } -export function getFormattedMetadataJSON(documentTextModel: NotebookTextModel, metadata: NotebookCellMetadata, language?: string) { +export function getFormattedMetadataJSON(documentTextModel: INotebookTextModel, metadata: NotebookCellMetadata, language?: string) { let filteredMetadata: { [key: string]: any } = {}; if (documentTextModel) { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 1672b6635b2..9de089e2f5a 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -8,8 +8,8 @@ import * as DOM from 'vs/base/browser/dom'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { IEditorOpenContext } from 'vs/workbench/common/editor'; -import { cellEditorBackground, getDefaultNotebookCreationOptions, notebookCellBorder, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { EditorPaneSelectionChangeReason, EditorPaneSelectionCompareResult, IEditorOpenContext, IEditorPaneSelection, IEditorPaneSelectionChangeEvent, IEditorPaneWithSelection } from 'vs/workbench/common/editor'; +import { cellEditorBackground, focusedEditorBorderColor, getDefaultNotebookCreationOptions, notebookCellBorder, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { NotebookDiffEditorInput } from '../notebookDiffEditorInput'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -42,10 +42,46 @@ import { FontMeasurements } from 'vs/editor/browser/config/fontMeasurements'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { cellIndexesToRanges, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; const $ = DOM.$; -export class NotebookTextDiffEditor extends EditorPane implements INotebookTextDiffEditor, INotebookDelegateForWebview { +class NotebookDiffEditorSelection implements IEditorPaneSelection { + + constructor( + private readonly selections: number[] + ) { } + + compare(other: IEditorPaneSelection): EditorPaneSelectionCompareResult { + if (!(other instanceof NotebookDiffEditorSelection)) { + return EditorPaneSelectionCompareResult.DIFFERENT; + } + + if (this.selections.length !== other.selections.length) { + return EditorPaneSelectionCompareResult.DIFFERENT; + } + + for (let i = 0; i < this.selections.length; i++) { + if (this.selections[i] !== other.selections[i]) { + return EditorPaneSelectionCompareResult.DIFFERENT; + } + } + + return EditorPaneSelectionCompareResult.IDENTICAL; + } + + restore(options: IEditorOptions): INotebookEditorOptions { + const notebookOptions: INotebookEditorOptions = { + cellSelections: cellIndexesToRanges(this.selections) + }; + + Object.assign(notebookOptions, options); + return notebookOptions; + } +} + +export class NotebookTextDiffEditor extends EditorPane implements INotebookTextDiffEditor, INotebookDelegateForWebview, IEditorPaneWithSelection { creationOptions: INotebookEditorCreationOptions = getDefaultNotebookCreationOptions(); static readonly ID: string = NOTEBOOK_DIFF_EDITOR_ID; @@ -86,6 +122,9 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD private _layoutCancellationTokenSource?: CancellationTokenSource; + private readonly _onDidChangeSelection = this._register(new Emitter<IEditorPaneSelectionChangeEvent>()); + readonly onDidChangeSelection = this._onDidChangeSelection.event; + private _isDisposed: boolean = false; get isDisposed() { @@ -110,6 +149,11 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._revealFirst = true; } + getSelection(): IEditorPaneSelection | undefined { + const selections = this._list.getFocus(); + return new NotebookDiffEditorSelection(selections); + } + toggleNotebookCellSelection(cell: IGenericCellViewModel) { // throw new Error('Method not implemented.'); } @@ -227,6 +271,8 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } })); + this._register(this._list.onDidChangeFocus(() => this._onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.USER }))); + // transparent cover this._webviewTransparentCover = DOM.append(this._list.rowsContainer, $('.webview-cover')); this._webviewTransparentCover.style.display = 'none'; @@ -333,7 +379,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._modifiedResourceDisposableStore.add(this._modifiedWebview); } - await this.updateLayout(this._layoutCancellationTokenSource.token); + await this.updateLayout(this._layoutCancellationTokenSource.token, options?.cellSelections ? cellRangesToIndexes(options.cellSelections) : undefined); } private _detachModel() { @@ -415,7 +461,14 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._originalWebview.element.style.left = `16px`; } - async updateLayout(token: CancellationToken) { + override setOptions(options: INotebookEditorOptions | undefined): void { + const selections = options?.cellSelections ? cellRangesToIndexes(options.cellSelections) : undefined; + if (selections) { + this._list.setFocus(selections); + } + } + + async updateLayout(token: CancellationToken, selections?: number[]) { if (!this._model) { return; } @@ -445,6 +498,10 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._list.setFocus([firstChangeIndex]); this._list.reveal(firstChangeIndex, 0.3); } + + if (selections) { + this._list.setFocus(selections); + } } private _isViewModelTheSame(viewModels: DiffElementViewModelBase[]) { @@ -929,6 +986,15 @@ registerThemingParticipant((theme, collector) => { }`); } + const focusCellBackgroundColor = theme.getColor(focusedEditorBorderColor); + + if (focusCellBackgroundColor) { + collector.addRule(`.notebook-text-diff-editor .monaco-list-row.focused .cell-body .border-container .top-border { border-top: 1px solid ${focusCellBackgroundColor};}`); + collector.addRule(`.notebook-text-diff-editor .monaco-list-row.focused .cell-body .border-container .bottom-border { border-top: 1px solid ${focusCellBackgroundColor};}`); + collector.addRule(`.notebook-text-diff-editor .monaco-list-row.focused .cell-body .border-container .left-border { border-left: 1px solid ${focusCellBackgroundColor};}`); + collector.addRule(`.notebook-text-diff-editor .monaco-list-row.focused .cell-body .border-container .right-border { border-right: 1px solid ${focusCellBackgroundColor};}`); + } + const diffDiagonalFillColor = theme.getColor(diffDiagonalFill); collector.addRule(` .notebook-text-diff-editor .diagonal-fill { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts index b774588523b..fead08ce397 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./notebookDiff'; -import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import * as DOM from 'vs/base/browser/dom'; -import { IListStyles, IStyleController } from 'vs/base/browser/ui/list/listWidget'; +import { IListOptions, IListStyles, isMonacoEditor, IStyleController, MouseController } from 'vs/base/browser/ui/list/listWidget'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -281,6 +281,17 @@ export class CellDiffSideBySideRenderer implements IListRenderer<SideBySideDiffE } } +export class NotebookMouseController<T> extends MouseController<T> { + protected override onViewPointer(e: IListMouseEvent<T>): void { + if (isMonacoEditor(e.browserEvent.target as HTMLElement)) { + const focus = typeof e.index === 'undefined' ? [] : [e.index]; + this.list.setFocus(focus, e.browserEvent); + } else { + super.onViewPointer(e); + } + } +} + export class NotebookTextDiffList extends WorkbenchList<DiffElementViewModelBase> implements IDisposable, IStyleController { private styleElement?: HTMLStyleElement; @@ -302,6 +313,10 @@ export class NotebookTextDiffList extends WorkbenchList<DiffElementViewModelBase super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); } + protected override createMouseController(options: IListOptions<DiffElementViewModelBase>): MouseController<DiffElementViewModelBase> { + return new NotebookMouseController(this); + } + getAbsoluteTopOfElement(element: DiffElementViewModelBase): number { const index = this.indexOf(element); // if (index === undefined || index < 0 || index >= this.length) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 9fdabf420e3..2b793c40961 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -29,7 +29,7 @@ import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEd import { isCompositeNotebookEditorInput, NotebookEditorInput, NotebookEditorInputOptions } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; -import { CellKind, CellUri, IResolvedNotebookEditorModel, NotebookDocumentBackupData, NotebookWorkingCopyTypeIdentifier, NotebookSetting, ICellOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellUri, IResolvedNotebookEditorModel, NotebookDocumentBackupData, NotebookWorkingCopyTypeIdentifier, NotebookSetting, ICellOutput, ICell } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; @@ -99,7 +99,6 @@ import { NotebookExecutionService } from 'vs/workbench/contrib/notebook/browser/ import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookKeymapService } from 'vs/workbench/contrib/notebook/common/notebookKeymapService'; import { NotebookKeymapService } from 'vs/workbench/contrib/notebook/browser/services/notebookKeymapServiceImpl'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; @@ -322,13 +321,16 @@ class CellContentProvider implements ITextModelContentProvider { } } - if (result) { - const once = Event.any(result.onWillDispose, ref.object.notebook.onWillDispose)(() => { - once.dispose(); - ref.dispose(); - }); + if (!result) { + ref.dispose(); + return null; } + const once = Event.any(result.onWillDispose, ref.object.notebook.onWillDispose)(() => { + once.dispose(); + ref.dispose(); + }); + return result; } } @@ -400,13 +402,16 @@ class CellInfoContentProvider { } } - if (result) { - const once = result.onWillDispose(() => { - once.dispose(); - ref.dispose(); - }); + if (!result) { + ref.dispose(); + return null; } + const once = result.onWillDispose(() => { + once.dispose(); + ref.dispose(); + }); + return result; } @@ -429,7 +434,7 @@ class CellInfoContentProvider { private _getResult(data: { notebook: URI; outputId?: string | undefined; - }, cell: NotebookCellTextModel) { + }, cell: ICell) { let result: { content: string; mode: ILanguageSelection } | undefined = undefined; const mode = this._languageService.createById('json'); @@ -472,34 +477,36 @@ class CellInfoContentProvider { const cell = ref.object.notebook.cells.find(cell => !!cell.outputs.find(op => op.outputId === data.outputId)); if (!cell) { + ref.dispose(); return null; } const result = this._getResult(data, cell); - if (result) { - const model = this._modelService.createModel(result.content, result.mode, resource); - const cellModelListener = Event.any(cell.onDidChangeOutputs, cell.onDidChangeOutputItems)(() => { - const newResult = this._getResult(data, cell); + if (!result) { + ref.dispose(); + return null; + } - if (!newResult) { - return; - } + const model = this._modelService.createModel(result.content, result.mode, resource); + const cellModelListener = Event.any(cell.onDidChangeOutputs ?? Event.None, cell.onDidChangeOutputItems ?? Event.None)(() => { + const newResult = this._getResult(data, cell); - model.setValue(newResult.content); - model.setMode(newResult.mode.languageId); - }); + if (!newResult) { + return; + } - const once = model.onWillDispose(() => { - once.dispose(); - cellModelListener.dispose(); - ref.dispose(); - }); + model.setValue(newResult.content); + model.setMode(newResult.mode.languageId); + }); - return model; - } + const once = model.onWillDispose(() => { + once.dispose(); + cellModelListener.dispose(); + ref.dispose(); + }); - return null; + return model; } } @@ -554,6 +561,15 @@ class NotebookEditorManager implements IWorkbenchContribution { group.closeEditors(staleInputs); } })); + + // CLOSE editors when we are about to open conflicting notebooks + this._disposables.add(_notebookEditorModelService.onWillFailWithConflict(e => { + for (const group of editorGroups.groups) { + const conflictInputs = group.editors.filter(input => input instanceof NotebookEditorInput && input.viewType !== e.viewType && isEqual(input.resource, e.resource)); + const p = group.closeEditors(conflictInputs); + e.waitUntil(p); + } + })); } dispose(): void { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index aed8ee62af8..7153bcfe26c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -26,6 +26,7 @@ import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKe import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { cellRangesToIndexes, ICellRange, reduceCellRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { IWebview } from 'vs/workbench/contrib/webview/browser/webview'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; //#region Shared commands export const EXPAND_CELL_INPUT_COMMAND_ID = 'notebook.cell.expandCellInput'; @@ -628,6 +629,11 @@ export interface IActiveNotebookEditor extends INotebookEditor { getNextVisibleCellIndex(index: number): number; } +export interface IBaseCellEditorOptions extends IDisposable { + readonly value: IEditorOptions; + readonly onDidChange: Event<void>; +} + /** * A mix of public interface and internal one (used by internal rendering code, e.g., cellRenderer) */ @@ -637,6 +643,7 @@ export interface INotebookEditorDelegate extends INotebookEditor { readonly creationOptions: INotebookEditorCreationOptions; readonly onDidChangeOptions: Event<void>; readonly onDidChangeDecorations: Event<void>; + getBaseCellEditorOptions(language: string): IBaseCellEditorOptions; createMarkupPreview(cell: ICellViewModel): Promise<void>; unhideMarkupPreviews(cells: readonly ICellViewModel[]): Promise<void>; hideMarkupPreviews(cells: readonly ICellViewModel[]): Promise<void>; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 5420ef359f9..623686523fa 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -49,7 +49,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti private _rootElement!: HTMLElement; private _dimension?: DOM.Dimension; - private readonly inputListener = this._register(new MutableDisposable()); + private readonly _inputListener = this._register(new MutableDisposable()); // override onDidFocus and onDidBlur to be based on the NotebookEditorWidget element private readonly _onDidFocusWidget = this._register(new Emitter<void>()); @@ -66,36 +66,36 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @IEditorService private readonly _editorService: IEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IEditorDropService private readonly _editorDropService: IEditorDropService, @INotebookEditorService private readonly _notebookWidgetService: INotebookEditorService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IFileService private readonly fileService: IFileService, + @IFileService private readonly _fileService: IFileService, @ITextResourceConfigurationService configurationService: ITextResourceConfigurationService ) { super(NotebookEditor.ID, telemetryService, themeService, storageService); this._editorMemento = this.getEditorMemento<INotebookEditorViewState>(_editorGroupService, configurationService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProvider(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProvider(e.scheme))); + this._register(this._fileService.onDidChangeFileSystemProviderCapabilities(e => this._onDidChangeFileSystemProvider(e.scheme))); + this._register(this._fileService.onDidChangeFileSystemProviderRegistrations(e => this._onDidChangeFileSystemProvider(e.scheme))); } - private onDidChangeFileSystemProvider(scheme: string): void { + private _onDidChangeFileSystemProvider(scheme: string): void { if (this.input instanceof NotebookEditorInput && this.input.resource?.scheme === scheme) { - this.updateReadonly(this.input); + this._updateReadonly(this.input); } } - private onDidChangeInputCapabilities(input: NotebookEditorInput): void { + private _onDidChangeInputCapabilities(input: NotebookEditorInput): void { if (this.input === input) { - this.updateReadonly(input); + this._updateReadonly(input); } } - private updateReadonly(input: NotebookEditorInput): void { + private _updateReadonly(input: NotebookEditorInput): void { if (this._widget.value) { this._widget.value.setOptions({ isReadOnly: input.hasCapability(EditorInputCapabilities.Readonly) }); } @@ -128,7 +128,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti override getActionViewItem(action: IAction): IActionViewItem | undefined { if (action.id === SELECT_KERNEL_ID) { // this is being disposed by the consumer - return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this); + return this._instantiationService.createInstance(NotebooKernelActionViewItem, action, this); } return undefined; } @@ -170,13 +170,13 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti return !!value && (DOM.isAncestor(activeElement, value.getDomNode() || DOM.isAncestor(activeElement, value.getOverflowContainerDomNode()))); } - override async setInput(input: NotebookEditorInput, options: INotebookEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> { + override async setInput(input: NotebookEditorInput, options: INotebookEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken, noRetry?: boolean): Promise<void> { try { clearMarks(input.resource); mark(input.resource, 'startTime'); const group = this.group!; - this.inputListener.value = input.onDidChangeCapabilities(() => this.onDidChangeInputCapabilities(input)); + this._inputListener.value = input.onDidChangeCapabilities(() => this._onDidChangeInputCapabilities(input)); this._widgetDisposableStore.clear(); @@ -186,7 +186,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti this._widget.value.onWillHide(); } - this._widget = <IBorrowValue<NotebookEditorWidget>>this.instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, input); + this._widget = <IBorrowValue<NotebookEditorWidget>>this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, input); this._widgetDisposableStore.add(this._widget.value!.onDidChangeModel(() => this._onDidChangeModel.fire())); this._widgetDisposableStore.add(this._widget.value!.onDidChangeActiveCell(() => this._onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.USER }))); @@ -205,6 +205,16 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti return undefined; } + // The widget has been taken away again. This can happen when the tab has been closed while + // loading was in progress, in particular when open the same resource as different view type. + // When this happen, retry once + if (!this._widget.value) { + if (noRetry) { + return undefined; + } + return this.setInput(input, options, context, token, true); + } + if (model === null) { throw new Error(localize('fail.noEditor', "Cannot open resource with notebook editor type '{0}', please check if you have the right extension installed and enabled.", input.viewType)); } @@ -314,7 +324,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti } override clearInput(): void { - this.inputListener.clear(); + this._inputListener.clear(); if (this._widget.value) { this._saveEditorViewState(this.input); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 4bb50abce37..aa1bcc7853f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -21,6 +21,7 @@ import { Color, RGBA } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { deepClone } from 'vs/base/common/objects'; import { setTimeout0 } from 'vs/base/common/platform'; import { extname, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; @@ -48,7 +49,7 @@ import { contrastBorder, diffInserted, diffRemoved, editorBackground, errorForeg import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PANEL_BORDER, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; -import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, IActiveNotebookEditorDelegate, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookWebviewMessage, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, IActiveNotebookEditorDelegate, IBaseCellEditorOptions, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookWebviewMessage, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { notebookDebug } from 'vs/workbench/contrib/notebook/browser/notebookLogger'; @@ -87,6 +88,100 @@ import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; const $ = DOM.$; +export class BaseCellEditorOptions extends Disposable implements IBaseCellEditorOptions { + private static fixedEditorOptions: IEditorOptions = { + scrollBeyondLastLine: false, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false, + alwaysConsumeMouseWheel: false + }, + renderLineHighlightOnlyWhenFocus: true, + overviewRulerLanes: 0, + selectOnLineNumbers: false, + lineNumbers: 'off', + lineDecorationsWidth: 0, + folding: true, + fixedOverflowWidgets: true, + minimap: { enabled: false }, + renderValidationDecorations: 'on', + lineNumbersMinChars: 3 + }; + + private _localDisposableStore = this._register(new DisposableStore()); + private readonly _onDidChange = this._register(new Emitter<void>()); + readonly onDidChange: Event<void> = this._onDidChange.event; + private _value: IEditorOptions; + + get value(): Readonly<IEditorOptions> { + return this._value; + } + + constructor(readonly notebookEditor: INotebookEditorDelegate, readonly notebookOptions: NotebookOptions, readonly configurationService: IConfigurationService, readonly language: string) { + super(); + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor') || e.affectsConfiguration('notebook')) { + this._recomputeOptions(); + } + })); + + this._register(notebookOptions.onDidChangeOptions(e => { + if (e.cellStatusBarVisibility || e.editorTopPadding || e.editorOptionsCustomizations) { + this._recomputeOptions(); + } + })); + + this._register(this.notebookEditor.onDidChangeModel(() => { + this._localDisposableStore.clear(); + + if (this.notebookEditor.hasModel()) { + this._localDisposableStore.add(this.notebookEditor.onDidChangeOptions(() => { + this._recomputeOptions(); + })); + + this._recomputeOptions(); + } + })); + + if (this.notebookEditor.hasModel()) { + this._localDisposableStore.add(this.notebookEditor.onDidChangeOptions(() => { + this._recomputeOptions(); + })); + } + + this._value = this._computeEditorOptions(); + } + + private _recomputeOptions(): void { + this._value = this._computeEditorOptions(); + this._onDidChange.fire(); + } + + private _computeEditorOptions() { + const editorOptions = deepClone(this.configurationService.getValue<IEditorOptions>('editor', { overrideIdentifier: this.language })); + const layoutConfig = this.notebookOptions.getLayoutConfiguration(); + const editorOptionsOverrideRaw = layoutConfig.editorOptionsCustomizations ?? {}; + const editorOptionsOverride: { [key: string]: any } = {}; + for (const key in editorOptionsOverrideRaw) { + if (key.indexOf('editor.') === 0) { + editorOptionsOverride[key.substring(7)] = editorOptionsOverrideRaw[key]; + } + } + const computed = Object.freeze({ + ...editorOptions, + ...BaseCellEditorOptions.fixedEditorOptions, + ...editorOptionsOverride, + ...{ padding: { top: 12, bottom: 12 } }, + readOnly: this.notebookEditor.isReadOnly + }); + + return computed; + } +} + export function getDefaultNotebookCreationOptions(): INotebookEditorCreationOptions { // We inlined the id to avoid loading comment contrib in tests const skipContributions = ['editor.contrib.review']; @@ -229,6 +324,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return this._list.visibleRanges || []; } + private _baseCellEditorOptions = new Map<string, IBaseCellEditorOptions>(); + readonly isEmbedded: boolean; private _readOnly: boolean; @@ -452,6 +549,19 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } //#region Editor Core + + getBaseCellEditorOptions(language: string): IBaseCellEditorOptions { + const existingOptions = this._baseCellEditorOptions.get(language); + + if (existingOptions) { + return existingOptions; + } else { + const options = new BaseCellEditorOptions(this, this.notebookOptions, this.configurationService, language); + this._baseCellEditorOptions.set(language, options); + return options; + } + } + private _updateForNotebookConfiguration() { if (!this._overlayContainer) { return; @@ -2809,6 +2919,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._overlayContainer.remove(); this.viewModel?.dispose(); + this._renderedEditors.clear(); + this._baseCellEditorOptions.forEach(v => v.dispose()); + this._baseCellEditorOptions.clear(); + + super.dispose(); + // unref this._webview = null; this._webviewResolvePromise = null; @@ -2817,11 +2933,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._listTopCellToolbar = null; this._notebookViewModel = undefined; this._cellContextKeyManager = null; - this._renderedEditors.clear(); + this._notebookTopToolbar = null!; + this._list = null!; + this._listViewInfoAccessor = null!; this._pendingLayouts = null; this._listDelegate = null; - - super.dispose(); } toJSON(): { notebookUri: URI | undefined } { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 1999bf283bc..374f1d707a1 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -594,12 +594,10 @@ export class NotebookService extends Disposable implements INotebookService { return this._registerProviderData(viewType, new SimpleNotebookProviderInfo(viewType, serializer, extensionData)); } - async withNotebookDataProvider(resource: URI, viewType?: string): Promise<ComplexNotebookProviderInfo | SimpleNotebookProviderInfo> { - const providers = this.notebookProviderInfoStore.getContributedNotebook(resource); - // If we have a viewtype specified we want that data provider, as the resource won't always map correctly - const selected = viewType ? providers.find(p => p.id === viewType) : providers[0]; + async withNotebookDataProvider(viewType: string): Promise<ComplexNotebookProviderInfo | SimpleNotebookProviderInfo> { + const selected = this.notebookProviderInfoStore.get(viewType); if (!selected) { - throw new Error(`NO contribution for resource: '${resource.toString()}'`); + throw new Error(`UNKNOWN notebook type '${viewType}'`); } await this.canResolve(selected.id); const result = this._notebookProviders.get(selected.id); @@ -714,4 +712,3 @@ export class NotebookService extends Disposable implements INotebookService { } } - diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts index 2f7413ad92f..8c0b8a99748 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts @@ -68,7 +68,7 @@ export class CellDragAndDropController extends Disposable { private readonly listOnWillScrollListener = this._register(new MutableDisposable()); constructor( - private readonly notebookEditor: INotebookEditorDelegate, + private notebookEditor: INotebookEditorDelegate, private readonly notebookListContainer: HTMLElement ) { super(); @@ -406,6 +406,11 @@ export class CellDragAndDropController extends Disposable { return this.getDropInsertDirection(dragPosRatio); } + + override dispose() { + this.notebookEditor = null!; + super.dispose(); + } } export function performCellDropEdits(editor: INotebookEditorDelegate, draggedCell: ICellViewModel, dropDirection: 'above' | 'below', draggedOverCell: ICellViewModel): void { @@ -494,6 +499,6 @@ export function performCellDropEdits(editor: INotebookEditorDelegate, draggedCel true, { kind: SelectionStateType.Index, focus: editor.getFocus(), selections: editor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: finalFocus, selections: [finalSelection] }), - undefined); + undefined, true); editor.revealCellRangeInView(finalSelection); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts index 00d68c38911..3059080c7d5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { deepClone } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import { IEditorOptions, LineNumbersType } from 'vs/editor/common/config/editorOptions'; import { localize } from 'vs/nls'; @@ -17,7 +15,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { Registry } from 'vs/platform/registry/common/platform'; import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; import { INotebookCellToolbarActionContext, INotebookCommandContext, NotebookMultiCellAction, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { IBaseCellEditorOptions, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { NotebookCellInternalMetadata, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -25,66 +23,18 @@ import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOp import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; export class CellEditorOptions extends CellPart { - private static fixedEditorOptions: IEditorOptions = { - scrollBeyondLastLine: false, - scrollbar: { - verticalScrollbarSize: 14, - horizontal: 'auto', - useShadows: true, - verticalHasArrows: false, - horizontalHasArrows: false, - alwaysConsumeMouseWheel: false - }, - renderLineHighlightOnlyWhenFocus: true, - overviewRulerLanes: 0, - selectOnLineNumbers: false, - lineNumbers: 'off', - lineDecorationsWidth: 0, - folding: true, - fixedOverflowWidgets: true, - minimap: { enabled: false }, - renderValidationDecorations: 'on', - lineNumbersMinChars: 3 - }; - - private _value: IEditorOptions; private _lineNumbers: 'on' | 'off' | 'inherit' = 'inherit'; private readonly _onDidChange = this._register(new Emitter<void>()); readonly onDidChange: Event<void> = this._onDidChange.event; - private _localDisposableStore = this._register(new DisposableStore()); + private _value: IEditorOptions; - constructor(readonly notebookEditor: INotebookEditorDelegate, readonly notebookOptions: NotebookOptions, readonly configurationService: IConfigurationService, readonly language: string) { + constructor(private readonly base: IBaseCellEditorOptions, readonly notebookOptions: NotebookOptions, readonly configurationService: IConfigurationService) { super(); - this._register(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('editor') || e.affectsConfiguration('notebook')) { - this._recomputeOptions(); - } - })); - this._register(notebookOptions.onDidChangeOptions(e => { - if (e.cellStatusBarVisibility || e.editorTopPadding || e.editorOptionsCustomizations) { - this._recomputeOptions(); - } + this._register(base.onDidChange(() => { + this._recomputeOptions(); })); - this._register(this.notebookEditor.onDidChangeModel(() => { - this._localDisposableStore.clear(); - - if (this.notebookEditor.hasModel()) { - this._localDisposableStore.add(this.notebookEditor.onDidChangeOptions(() => { - this._recomputeOptions(); - })); - - this._recomputeOptions(); - } - })); - - if (this.notebookEditor.hasModel()) { - this._localDisposableStore.add(this.notebookEditor.onDidChangeOptions(() => { - this._recomputeOptions(); - })); - } - this._value = this._computeEditorOptions(); } @@ -102,25 +52,17 @@ export class CellEditorOptions extends CellPart { private _computeEditorOptions() { const renderLineNumbers = this.configurationService.getValue<'on' | 'off'>('notebook.lineNumbers') === 'on'; const lineNumbers: LineNumbersType = renderLineNumbers ? 'on' : 'off'; - const editorOptions = deepClone(this.configurationService.getValue<IEditorOptions>('editor', { overrideIdentifier: this.language })); - const layoutConfig = this.notebookOptions.getLayoutConfiguration(); - const editorOptionsOverrideRaw = layoutConfig.editorOptionsCustomizations ?? {}; - const editorOptionsOverride: { [key: string]: any } = {}; - for (const key in editorOptionsOverrideRaw) { - if (key.indexOf('editor.') === 0) { - editorOptionsOverride[key.substring(7)] = editorOptionsOverrideRaw[key]; - } - } - const computed = { - ...editorOptions, - ...CellEditorOptions.fixedEditorOptions, - ... { lineNumbers }, - ...editorOptionsOverride, - ...{ padding: { top: 12, bottom: 12 } }, - readOnly: this.notebookEditor.isReadOnly - }; - return computed; + const value = this.base.value; + + if (value.lineNumbers !== lineNumbers) { + return { + ...value, + ...{ lineNumbers } + }; + } else { + return Object.assign({}, value); + } } getUpdatedValue(internalMetadata: NotebookCellInternalMetadata, cellUri: URI): IEditorOptions { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts index fab7ccb3505..3f2df96e305 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -53,8 +53,8 @@ export class CodeCell extends Disposable { ) { super(); - const cellEditorOptions = this._register(new CellEditorOptions(this.notebookEditor, this.notebookEditor.notebookOptions, this.configurationService, this.viewCell.language)); - this._outputContainerRenderer = this.instantiationService.createInstance(CellOutputContainer, notebookEditor, viewCell, templateData, { limit: 2 }); + const cellEditorOptions = this._register(new CellEditorOptions(this.notebookEditor.getBaseCellEditorOptions(viewCell.language), this.notebookEditor.notebookOptions, this.configurationService)); + this._outputContainerRenderer = this.instantiationService.createInstance(CellOutputContainer, notebookEditor, viewCell, templateData, { limit: 500 }); this.cellParts = [...templateData.cellParts, cellEditorOptions, this._outputContainerRenderer]; const editorHeight = this.calculateInitEditorHeight(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts index b1b5077afea..bf119feb15c 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts @@ -59,8 +59,7 @@ export class StatefulMarkdownCell extends Disposable { this.constructDOM(); this.editorPart = templateData.editorPart; - - this.cellEditorOptions = this._register(new CellEditorOptions(this.notebookEditor, this.notebookEditor.notebookOptions, this.configurationService, this.viewCell.language)); + this.cellEditorOptions = this._register(new CellEditorOptions(this.notebookEditor.getBaseCellEditorOptions(viewCell.language), this.notebookEditor.notebookOptions, this.configurationService)); this.cellEditorOptions.setLineNumbers(this.viewCell.lineNumbers); this.editorOptions = this.cellEditorOptions.getValue(this.viewCell.internalMetadata, this.viewCell.uri); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 673fe4a4312..3ef1c58649e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -1252,8 +1252,20 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID this.view.setScrollTop(this.view.elementTop(viewIndex)); break; case CellRevealPosition.Center: - this.view.setScrollTop(elementTop - this.view.renderHeight / 2); - this.view.setScrollTop(this.view.elementTop(viewIndex) - this.view.renderHeight / 2); + { + // reveal the cell top in the viewport center initially + this.view.setScrollTop(elementTop - this.view.renderHeight / 2); + // cell rendered already, we now have a more accurate cell height + const newElementTop = this.view.elementTop(viewIndex); + const newElementHeight = this.view.elementHeight(viewIndex); + const renderHeight = this.getViewScrollBottom() - this.getViewScrollTop(); + if (newElementHeight >= renderHeight) { + // cell is larger than viewport, reveal top + this.view.setScrollTop(newElementTop); + } else { + this.view.setScrollTop(newElementTop + (newElementHeight / 2) - (renderHeight / 2)); + } + } break; case CellRevealPosition.Bottom: this.view.setScrollTop(this.scrollTop + (elementBottom - wrapperBottom)); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 1beeb548519..02beef093cf 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -98,7 +98,7 @@ abstract class AbstractCellRenderer { language: string, protected dndController: CellDragAndDropController | undefined ) { - this.editorOptions = new CellEditorOptions(notebookEditor, notebookEditor.notebookOptions, configurationService, language); + this.editorOptions = new CellEditorOptions(this.notebookEditor.getBaseCellEditorOptions(language), this.notebookEditor.notebookOptions, configurationService); } dispose() { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 11b51e17e2f..8b7c306af28 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -156,6 +156,8 @@ export abstract class BaseCellViewModel extends Disposable { this._onDidChangeState.fire({ outputCollapsedChanged: true }); } + private _textEditorRestore: any; + constructor( readonly viewType: string, readonly model: NotebookCellTextModel, @@ -234,7 +236,9 @@ export abstract class BaseCellViewModel extends Disposable { this._textEditor = editor; if (this._editorViewStates) { - this._restoreViewState(this._editorViewStates); + this._textEditorRestore = setTimeout(() => { + this._restoreViewState(this._editorViewStates); + }); } if (this._editorTransientState) { @@ -260,6 +264,7 @@ export abstract class BaseCellViewModel extends Disposable { } detachTextEditor() { + clearTimeout(this._textEditorRestore); this.saveViewState(); this.saveTransientState(); // decorations need to be cleared first as editors can be resued. @@ -584,6 +589,7 @@ export abstract class BaseCellViewModel extends Disposable { super.dispose(); dispose(this._editorListeners); + clearTimeout(this._textEditorRestore); // Only remove the undo redo stack if we map this cell uri to itself // If we are not in perCell mode, it will map to the full NotebookDocument and diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts index f897a762e74..243b4b96edc 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts @@ -602,6 +602,17 @@ export class NotebookEditorToolbar extends Disposable { } this._computeSizes(); } + + override dispose() { + this._notebookLeftToolbar.context = undefined; + this._notebookRightToolbar.context = undefined; + this._notebookLeftToolbar.dispose(); + this._notebookRightToolbar.dispose(); + this._notebookLeftToolbar = null!; + this._notebookRightToolbar = null!; + + super.dispose(); + } } registerThemingParticipant((theme, collector) => { diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index a04f9eb9ecf..5f186e4b0da 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { flatten } from 'vs/base/common/arrays'; import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -324,7 +323,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel case 'language': this._pauseableEmitter.fire({ - rawEvents: [{ kind: NotebookCellsChangeType.ChangeLanguage, index: this._getCellIndexByHandle(cell.handle), language: cell.language, transient: false }], + rawEvents: [{ kind: NotebookCellsChangeType.ChangeCellLanguage, index: this._getCellIndexByHandle(cell.handle), language: cell.language, transient: false }], versionId: this.versionId, synchronous: true, endSelectionState: undefined @@ -400,11 +399,12 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel ], true, undefined, () => undefined, - undefined + undefined, + true ); } - applyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, beginSelectionState: ISelectionState | undefined, endSelectionsComputer: () => ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, computeUndoRedo: boolean = true): boolean { + applyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, beginSelectionState: ISelectionState | undefined, endSelectionsComputer: () => ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, computeUndoRedo: boolean): boolean { this._pauseableEmitter.pause(); this.pushStackElement('edit', beginSelectionState, undoRedoGroup); @@ -502,7 +502,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return [...otherEdits.reverse(), ...replaceEdits]; }); - const flattenEdits = flatten(edits); + const flattenEdits = edits.flat(); for (const { edit, cellIndex } of flattenEdits) { switch (edit.editType) { @@ -937,7 +937,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } this._pauseableEmitter.fire({ - rawEvents: [{ kind: NotebookCellsChangeType.ChangeLanguage, index: this._cells.indexOf(cell), language: languageId, transient: false }], + rawEvents: [{ kind: NotebookCellsChangeType.ChangeCellLanguage, index: this._cells.indexOf(cell), language: languageId, transient: false }], versionId: this.versionId, synchronous: true, endSelectionState: undefined diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index c5ffb1bbf2f..bc1f504dc36 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -5,7 +5,7 @@ import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDiffResult, ISequence } from 'vs/base/common/diff/diff'; +import { IDiffResult } from 'vs/base/common/diff/diff'; import { Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; import { Iterable } from 'vs/base/common/iterator'; @@ -18,11 +18,13 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { ILineChange } from 'vs/editor/common/diff/diffComputer'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { Command } from 'vs/editor/common/languages'; +import { IReadonlyTextBuffer } from 'vs/editor/common/model'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; +import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; @@ -214,7 +216,10 @@ export interface ICell { outputs: ICellOutput[]; metadata: NotebookCellMetadata; internalMetadata: NotebookCellInternalMetadata; + getHashValue(): number; + textBuffer: IReadonlyTextBuffer; onDidChangeOutputs?: Event<NotebookCellOutputsSplice>; + onDidChangeOutputItems?: Event<void>; onDidChangeLanguage: Event<string>; onDidChangeMetadata: Event<void>; onDidChangeInternalMetadata: Event<CellInternalMetadataChangedEvent>; @@ -223,10 +228,14 @@ export interface ICell { export interface INotebookTextModel { readonly viewType: string; metadata: NotebookDocumentMetadata; + readonly transientOptions: TransientOptions; readonly uri: URI; readonly versionId: number; - + readonly length: number; readonly cells: readonly ICell[]; + reset(cells: ICellDto2[], metadata: NotebookDocumentMetadata, transientOptions: TransientOptions): void; + applyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, beginSelectionState: ISelectionState | undefined, endSelectionsComputer: () => ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, computeUndoRedo?: boolean): boolean; + onDidChangeContent: Event<NotebookTextModelChangedEvent>; onWillDispose: Event<void>; } @@ -257,7 +266,7 @@ export interface IMainCellDto { export enum NotebookCellsChangeType { ModelChange = 1, Move = 2, - ChangeLanguage = 5, + ChangeCellLanguage = 5, Initialize = 6, ChangeCellMetadata = 7, Output = 8, @@ -308,7 +317,7 @@ export interface NotebookOutputItemChangedEvent { } export interface NotebookCellsChangeLanguageEvent { - readonly kind: NotebookCellsChangeType.ChangeLanguage; + readonly kind: NotebookCellsChangeType.ChangeCellLanguage; readonly index: number; readonly language: string; } @@ -775,7 +784,7 @@ export interface INotebookEditorModel extends IEditorModel { readonly onDidChangeReadonly: Event<void>; readonly resource: URI; readonly viewType: string; - readonly notebook: NotebookTextModel | undefined; + readonly notebook: INotebookTextModel | undefined; isResolved(): this is IResolvedNotebookEditorModel; isDirty(): boolean; isReadonly(): boolean; @@ -869,20 +878,6 @@ export interface INotebookCellStatusBarItemProvider { provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken): Promise<INotebookCellStatusBarItemList | undefined>; } -export class CellSequence implements ISequence { - - constructor(readonly textModel: NotebookTextModel) { - } - - getElements(): string[] | number[] | Int32Array { - const hashValue = new Int32Array(this.textModel.cells.length); - for (let i = 0; i < this.textModel.cells.length; i++) { - hashValue[i] = this.textModel.cells[i].getHashValue(); - } - - return hashValue; - } -} export interface INotebookDiffResult { cellsDiff: IDiffResult; diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index 31ed15060f2..cffd2b0362d 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -262,7 +262,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } if (this.options._backupId) { - const info = await this._notebookService.withNotebookDataProvider(this._editorModelReference.object.notebook.uri, this._editorModelReference.object.notebook.viewType); + const info = await this._notebookService.withNotebookDataProvider(this._editorModelReference.object.notebook.viewType); if (!(info instanceof SimpleNotebookProviderInfo)) { throw new Error('CANNOT open file notebook with this provider'); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 9e3e00d91ee..8938cff2e80 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -665,7 +665,7 @@ export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCo async createModel(resource: URI, stream: VSBufferReadableStream, token: CancellationToken): Promise<NotebookFileWorkingCopyModel> { - const info = await this._notebookService.withNotebookDataProvider(resource, this._viewType); + const info = await this._notebookService.withNotebookDataProvider(this._viewType); if (!(info instanceof SimpleNotebookProviderInfo)) { throw new Error('CANNOT open file notebook with this provider'); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts index b2039e2694b..e2d2a6c4fe4 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts @@ -7,10 +7,21 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { URI } from 'vs/base/common/uri'; import { IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IReference } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; +import { Event, IWaitUntil } from 'vs/base/common/event'; export const INotebookEditorModelResolverService = createDecorator<INotebookEditorModelResolverService>('INotebookModelResolverService'); +/** + * A notebook file can only be opened ONCE per notebook type. + * This event fires when a file is already open as type A + * and there is request to open it as type B. Listeners must + * do cleanup (close editor, release references) or the request fails + */ +export interface INotebookConflictEvent extends IWaitUntil { + resource: URI; + viewType: string; +} + export interface IUntitledNotebookResource { /** * Depending on the value of `untitledResource` will @@ -34,6 +45,8 @@ export interface INotebookEditorModelResolverService { readonly onDidSaveNotebook: Event<URI>; readonly onDidChangeDirty: Event<IResolvedNotebookEditorModel>; + readonly onWillFailWithConflict: Event<INotebookConflictEvent>; + isDirty(resource: URI): boolean; resolve(resource: URI, viewType?: string): Promise<IReference<IResolvedNotebookEditorModel>>; diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index f5d98feee26..bc34431e3f5 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -10,15 +10,16 @@ import { ComplexNotebookEditorModel, NotebookFileWorkingCopyModel, NotebookFileW import { combinedDisposable, DisposableStore, dispose, IDisposable, IReference, ReferenceCollection, toDisposable } from 'vs/base/common/lifecycle'; import { ComplexNotebookProviderInfo, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { ILogService } from 'vs/platform/log/common/log'; -import { Emitter, Event } from 'vs/base/common/event'; +import { AsyncEmitter, Emitter, Event } from 'vs/base/common/event'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { INotebookEditorModelResolverService, IUntitledNotebookResource } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { INotebookConflictEvent, INotebookEditorModelResolverService, IUntitledNotebookResource } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { ResourceMap } from 'vs/base/common/map'; import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { Schemas } from 'vs/base/common/network'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { assertIsDefined } from 'vs/base/common/types'; +import { CancellationToken } from 'vs/base/common/cancellation'; class NotebookModelReferenceCollection extends ReferenceCollection<Promise<IResolvedNotebookEditorModel>> { @@ -61,7 +62,7 @@ class NotebookModelReferenceCollection extends ReferenceCollection<Promise<IReso protected async createReferencedObject(key: string, viewType: string, hasAssociatedFilePath: boolean): Promise<IResolvedNotebookEditorModel> { const uri = URI.parse(key); - const info = await this._notebookService.withNotebookDataProvider(uri, viewType); + const info = await this._notebookService.withNotebookDataProvider(viewType); let result: IResolvedNotebookEditorModel; @@ -136,6 +137,9 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes readonly onDidSaveNotebook: Event<URI>; readonly onDidChangeDirty: Event<IResolvedNotebookEditorModel>; + private readonly _onWillFailWithConflict = new AsyncEmitter<INotebookConflictEvent>(); + readonly onWillFailWithConflict = this._onWillFailWithConflict.event; + constructor( @IInstantiationService instantiationService: IInstantiationService, @INotebookService private readonly _notebookService: INotebookService, @@ -208,7 +212,14 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes } if (existingViewType && existingViewType !== viewType) { - throw new Error(`A notebook with view type '${existingViewType}' already exists for '${resource}', CANNOT create another notebook with view type ${viewType}`); + + await this._onWillFailWithConflict.fireAsync({ resource, viewType }, CancellationToken.None); + + // check again, listener should have done cleanup + const existingViewType2 = this._notebookService.getNotebookTextModel(resource)?.viewType; + if (existingViewType2 && existingViewType2 !== viewType) { + throw new Error(`A notebook with view type '${existingViewType2}' already exists for '${resource}', CANNOT create another notebook with view type ${viewType}`); + } } const reference = this._data.acquire(resource.toString(), viewType, hasAssociatedFilePath); diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 305f67e5cbb..18faf5fdee5 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -68,7 +68,7 @@ export interface INotebookService { registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: INotebookContentProvider): IDisposable; registerNotebookSerializer(viewType: string, extensionData: NotebookExtensionDescription, serializer: INotebookSerializer): IDisposable; - withNotebookDataProvider(resource: URI, viewType?: string): Promise<ComplexNotebookProviderInfo | SimpleNotebookProviderInfo>; + withNotebookDataProvider(viewType: string): Promise<ComplexNotebookProviderInfo | SimpleNotebookProviderInfo>; getOutputMimeTypeInfo(textModel: NotebookTextModel, kernelProvides: readonly string[] | undefined, output: IOutputDto): readonly IOrderedMimeType[]; diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts index ace5f4f4c15..51b61d425b1 100644 --- a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts +++ b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts @@ -122,7 +122,7 @@ class MirrorNotebookDocument { } else if (e.kind === NotebookCellsChangeType.Output) { const cell = this.cells[e.index]; cell.outputs = e.outputs; - } else if (e.kind === NotebookCellsChangeType.ChangeLanguage) { + } else if (e.kind === NotebookCellsChangeType.ChangeCellLanguage) { const cell = this.cells[e.index]; cell.language = e.language; } else if (e.kind === NotebookCellsChangeType.ChangeCellMetadata) { diff --git a/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts b/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts index 5a469da5487..08fd7b6e023 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; -import { changeCellToKind, computeCellLinesContents, copyCellRange, joinNotebookCells, moveCellRange, moveCellToIdx, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; +import { changeCellToKind, computeCellLinesContents, copyCellRange, joinNotebookCells, moveCellRange, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CellEditType, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { Range } from 'vs/editor/common/core/range'; @@ -13,56 +13,6 @@ import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; suite('CellOperations', () => { - test('move cells down', async function () { - await withTestNotebook( - [ - ['//a', 'javascript', CellKind.Code, [], {}], - ['//b', 'javascript', CellKind.Code, [], {}], - ['//c', 'javascript', CellKind.Code, [], {}], - ], - (editor, viewModel) => { - moveCellToIdx(editor, 0, 1, 0, true); - // no-op - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//a'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//b'); - - moveCellToIdx(editor, 0, 1, 1, true); - // b, a, c - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//b'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//a'); - assert.strictEqual(viewModel.cellAt(2)?.getText(), '//c'); - - moveCellToIdx(editor, 0, 1, 2, true); - // a, c, b - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//a'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//c'); - assert.strictEqual(viewModel.cellAt(2)?.getText(), '//b'); - } - ); - }); - - test('move cells up', async function () { - await withTestNotebook( - [ - ['//a', 'javascript', CellKind.Code, [], {}], - ['//b', 'javascript', CellKind.Code, [], {}], - ['//c', 'javascript', CellKind.Code, [], {}], - ], - (editor, viewModel) => { - moveCellToIdx(editor, 1, 1, 0, true); - // b, a, c - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//b'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//a'); - - moveCellToIdx(editor, 2, 1, 0, true); - // c, b, a - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//c'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//b'); - assert.strictEqual(viewModel.cellAt(2)?.getText(), '//a'); - } - ); - }); - test('Move cells - single cell', async function () { await withTestNotebook( [ diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts index 3a543fd6a4a..0e898675d75 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts @@ -113,7 +113,7 @@ suite('Notebook Outline', function () { ], outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); - assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '!@#$\n Überschrïft'); + assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '!@#$'); }); }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts index 2b59cccb488..8cc256883a7 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts @@ -5,14 +5,30 @@ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; -import { LcsDiff } from 'vs/base/common/diff/diff'; +import { ISequence, LcsDiff } from 'vs/base/common/diff/diff'; import { Mimes } from 'vs/base/common/mime'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor'; -import { CellKind, CellSequence } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, INotebookTextModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebookDiffModel } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +class CellSequence implements ISequence { + + constructor(readonly textModel: INotebookTextModel) { + } + + getElements(): string[] | number[] | Int32Array { + const hashValue = new Int32Array(this.textModel.cells.length); + for (let i = 0; i < this.textModel.cells.length; i++) { + hashValue[i] = this.textModel.cells[i].getHashValue(); + } + + return hashValue; + } +} + + suite('NotebookCommon', () => { const configurationService = new TestConfigurationService(); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts index aebc72563ab..6f65268830f 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts @@ -68,7 +68,7 @@ suite('NotebookExecutionService', () => { async (viewModel) => { const executionService = instantiationService.createInstance(NotebookExecutionService); - const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); await assertThrowsAsync(async () => await executionService.executeNotebookCell(cell)); }); }); @@ -80,7 +80,7 @@ suite('NotebookExecutionService', () => { kernelService.registerKernel(new TestNotebookKernel({ languages: ['testlang'] })); const executionService = instantiationService.createInstance(NotebookExecutionService); - const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); await assertThrowsAsync(async () => await executionService.executeNotebookCell(cell)); }); @@ -96,7 +96,7 @@ suite('NotebookExecutionService', () => { const executeSpy = sinon.spy(); kernel.executeNotebookCellsRequest = executeSpy; - const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); await executionService.executeNotebookCells(viewModel.notebookDocument, [cell]); assert.strictEqual(executeSpy.calledOnce, true); }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts index 42a2b1c6d2b..0bee94ca7a1 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts @@ -40,7 +40,7 @@ suite('NotebookTextModel', () => { textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)] }, { editType: CellEditType.Replace, index: 3, count: 0, cells: [new TestCell(textModel.viewType, 6, 'var f = 6;', 'javascript', CellKind.Code, [], languageService)] }, - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 6); @@ -63,7 +63,7 @@ suite('NotebookTextModel', () => { textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)] }, { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(textModel.viewType, 6, 'var f = 6;', 'javascript', CellKind.Code, [], languageService)] }, - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 6); @@ -86,7 +86,7 @@ suite('NotebookTextModel', () => { textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, { editType: CellEditType.Replace, index: 3, count: 1, cells: [] }, - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells[0].getValue(), 'var a = 1;'); assert.strictEqual(textModel.cells[1].getValue(), 'var c = 3;'); @@ -107,7 +107,7 @@ suite('NotebookTextModel', () => { textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, { editType: CellEditType.Replace, index: 3, count: 0, cells: [new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)] }, - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 4); assert.strictEqual(textModel.cells[0].getValue(), 'var a = 1;'); @@ -129,7 +129,7 @@ suite('NotebookTextModel', () => { textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)] }, - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 4); assert.strictEqual(textModel.cells[0].getValue(), 'var a = 1;'); @@ -151,7 +151,7 @@ suite('NotebookTextModel', () => { const textModel = editor.textModel; textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 1, cells: [new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)] }, - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 4); assert.strictEqual(textModel.cells[0].getValue(), 'var a = 1;'); @@ -175,7 +175,7 @@ suite('NotebookTextModel', () => { index: Number.MAX_VALUE, editType: CellEditType.Output, outputs: [] - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); }); // invalid index 2 @@ -184,7 +184,7 @@ suite('NotebookTextModel', () => { index: -1, editType: CellEditType.Output, outputs: [] - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); }); textModel.applyEdits([{ @@ -194,7 +194,7 @@ suite('NotebookTextModel', () => { outputId: 'someId', outputs: [{ mime: Mimes.markdown, data: valueBytesFromString('_Hello_') }] }] - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 1); assert.strictEqual(textModel.cells[0].outputs.length, 1); @@ -208,7 +208,7 @@ suite('NotebookTextModel', () => { outputId: 'someId2', outputs: [{ mime: Mimes.markdown, data: valueBytesFromString('_Hello2_') }] }] - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 1); assert.strictEqual(textModel.cells[0].outputs.length, 2); @@ -224,7 +224,7 @@ suite('NotebookTextModel', () => { outputId: 'someId3', outputs: [{ mime: Mimes.text, data: valueBytesFromString('Last, replaced output') }] }] - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 1); assert.strictEqual(textModel.cells[0].outputs.length, 1); @@ -262,7 +262,7 @@ suite('NotebookTextModel', () => { outputs: [{ mime: Mimes.markdown, data: valueBytesFromString('append 2') }] }] } - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 1); assert.strictEqual(textModel.cells[0].outputs.length, 2); @@ -299,7 +299,7 @@ suite('NotebookTextModel', () => { mime: Mimes.markdown, data: valueBytesFromString('append 2') }] } - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 1); assert.strictEqual(textModel.cells[0].outputs.length, 1, 'has 1 output'); @@ -324,7 +324,7 @@ suite('NotebookTextModel', () => { index: Number.MAX_VALUE, editType: CellEditType.Metadata, metadata: {} - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); }); // invalid index 2 @@ -333,20 +333,20 @@ suite('NotebookTextModel', () => { index: -1, editType: CellEditType.Metadata, metadata: {} - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); }); textModel.applyEdits([{ index: 0, editType: CellEditType.Metadata, metadata: { customProperty: 15 }, - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); textModel.applyEdits([{ index: 0, editType: CellEditType.Metadata, metadata: {}, - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 1); assert.strictEqual(textModel.cells[0].metadata.customProperty, undefined); @@ -366,13 +366,13 @@ suite('NotebookTextModel', () => { index: 0, editType: CellEditType.PartialMetadata, metadata: { customProperty: 15 }, - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); textModel.applyEdits([{ index: 0, editType: CellEditType.PartialMetadata, metadata: {}, - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 1); assert.strictEqual(textModel.cells[0].metadata.customProperty, 15); @@ -403,7 +403,7 @@ suite('NotebookTextModel', () => { textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)] }, - ], true, undefined, () => ({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }), undefined); + ], true, undefined, () => ({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }), undefined, true); assert.strictEqual(textModel.cells.length, 4); assert.strictEqual(textModel.cells[0].getValue(), 'var a = 1;'); @@ -449,7 +449,7 @@ suite('NotebookTextModel', () => { editType: CellEditType.Metadata, metadata: {}, } - ], true, undefined, () => ({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }), undefined); + ], true, undefined, () => ({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }), undefined, true); assert.notStrictEqual(changeEvent, undefined); assert.strictEqual(changeEvent!.rawEvents.length, 2); @@ -641,7 +641,7 @@ suite('NotebookTextModel', () => { } ]; - editor.textModel.applyEdits(edits, true, undefined, () => undefined, undefined); + editor.textModel.applyEdits(edits, true, undefined, () => undefined, undefined, true); assert.strictEqual(notebook.cells[0].outputs.length, 1); assert.strictEqual(notebook.cells[0].outputs[0].outputs.length, 2); @@ -671,7 +671,7 @@ suite('NotebookTextModel', () => { } ]; - editor.textModel.applyEdits(edits, true, undefined, () => undefined, undefined); + editor.textModel.applyEdits(edits, true, undefined, () => undefined, undefined, true); assert.strictEqual(notebook.cells.length, 2); assert.strictEqual(notebook.cells[0].outputs.length, 0); @@ -707,7 +707,7 @@ suite('NotebookTextModel', () => { } ]; - editor.textModel.applyEdits(edits, true, undefined, () => undefined, undefined); + editor.textModel.applyEdits(edits, true, undefined, () => undefined, undefined, true); assert.strictEqual(notebook.cells.length, 2); assert.strictEqual(notebook.cells[0].outputs.length, 1); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts index ebc4e1f9cc6..becb17a1256 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts @@ -91,13 +91,13 @@ suite('NotebookViewModel', () => { const lastViewCell = viewModel.cellAt(viewModel.length - 1)!; const insertIndex = viewModel.getCellIndex(firstViewCell) + 1; - const cell = insertCellAtIndex(viewModel, insertIndex, 'var c = 3;', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, insertIndex, 'var c = 3;', 'javascript', CellKind.Code, {}, [], true, true); const addedCellIndex = viewModel.getCellIndex(cell); runDeleteAction(editor, viewModel.cellAt(addedCellIndex)!); const secondInsertIndex = viewModel.getCellIndex(lastViewCell) + 1; - const cell2 = insertCellAtIndex(viewModel, secondInsertIndex, 'var d = 4;', 'javascript', CellKind.Code, {}, [], true); + const cell2 = insertCellAtIndex(viewModel, secondInsertIndex, 'var d = 4;', 'javascript', CellKind.Code, {}, [], true, true); assert.strictEqual(viewModel.length, 3); assert.strictEqual(viewModel.notebookDocument.cells.length, 3); @@ -150,7 +150,7 @@ suite('NotebookViewModel Decorations', () => { end: 2, }); - insertCellAtIndex(viewModel, 0, 'var d = 6;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 0, 'var d = 6;', 'javascript', CellKind.Code, {}, [], true, true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 2, @@ -164,7 +164,7 @@ suite('NotebookViewModel Decorations', () => { end: 2 }); - insertCellAtIndex(viewModel, 3, 'var d = 7;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 3, 'var d = 7;', 'javascript', CellKind.Code, {}, [], true, true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1, @@ -207,14 +207,14 @@ suite('NotebookViewModel Decorations', () => { end: 3 }); - insertCellAtIndex(viewModel, 5, 'var d = 9;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 5, 'var d = 9;', 'javascript', CellKind.Code, {}, [], true, true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1, end: 3 }); - insertCellAtIndex(viewModel, 4, 'var d = 10;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 4, 'var d = 10;', 'javascript', CellKind.Code, {}, [], true, true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1, diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index fd0d94ee157..514c5231443 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -93,7 +93,7 @@ export class NotebookEditorTestModel extends EditorModel implements INotebookEdi return this._notebook.uri; } - get notebook() { + get notebook(): NotebookTextModel { return this._notebook; } diff --git a/src/vs/workbench/contrib/output/browser/media/output.css b/src/vs/workbench/contrib/output/browser/media/output.css index f80049ed1dc..ea0ee3e8d6b 100644 --- a/src/vs/workbench/contrib/output/browser/media/output.css +++ b/src/vs/workbench/contrib/output/browser/media/output.css @@ -30,9 +30,7 @@ } .monaco-workbench .part.sidebar > .title > .title-actions .switch-output > .monaco-select-box { - border: none !important; display: block !important; - background-color: unset !important; } .monaco-pane-view .pane > .pane-header .monaco-action-bar .switch-output.action-item.select-container { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index b49b69c4acd..77942bbbf0a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -152,7 +152,7 @@ export class SettingsEditor2 extends EditorPane { private controlsElement!: HTMLElement; private settingsTargetsWidget!: SettingsTargetsWidget; - private splitView!: SplitView; + private splitView!: SplitView<number>; private settingsTreeContainer!: HTMLElement; private settingsTree!: SettingsTree; @@ -712,9 +712,9 @@ export class SettingsEditor2 extends EditorPane { element: this.tocTreeContainer, minimumSize: SettingsEditor2.TOC_MIN_WIDTH, maximumSize: Number.POSITIVE_INFINITY, - layout: (width) => { + layout: (width, _, height) => { this.tocTreeContainer.style.width = `${width}px`; - this.tocTree.layout(undefined, width); + this.tocTree.layout(height, width); } }, startingWidth, undefined, true); this.splitView.addView({ @@ -722,9 +722,9 @@ export class SettingsEditor2 extends EditorPane { element: this.settingsTreeContainer, minimumSize: SettingsEditor2.EDITOR_MIN_WIDTH, maximumSize: Number.POSITIVE_INFINITY, - layout: (width) => { + layout: (width, _, height) => { this.settingsTreeContainer.style.width = `${width}px`; - this.settingsTree.layout(undefined, width); + this.settingsTree.layout(height, width); } }, Sizing.Distribute, undefined, true); this._register(this.splitView.onDidSashReset(() => { @@ -1008,6 +1008,25 @@ export class SettingsEditor2 extends EditorPane { } private reportModifiedSetting(props: { key: string; query: string; searchResults: ISearchResult[] | null; rawResults: ISearchResult[] | null; showConfiguredOnly: boolean; isReset: boolean; settingsTarget: SettingsTarget }): void { + type SettingsEditorModifiedSettingEvent = { + key: string; + groupId: string | undefined; + nlpIndex: number | undefined; + displayIndex: number | undefined; + showConfiguredOnly: boolean; + isReset: boolean; + target: string; + }; + type SettingsEditorModifiedSettingClassification = { + key: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'The setting that is being modified.' }; + groupId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'Whether the setting is from the local search or remote search provider, if applicable.' }; + nlpIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; comment: 'The index of the setting in the remote search provider results, if applicable.' }; + displayIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; comment: 'The index of the setting in the combined search results, if applicable.' }; + showConfiguredOnly: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; 'owner': 'rzhao271'; comment: 'Whether the user is in the modified view, which shows configured settings only.' }; + isReset: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'Identifies whether a setting was reset to its default value.' }; + target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'The scope of the setting, such as user or workspace.' }; + }; + this.pendingSettingUpdate = null; let groupId: string | undefined = undefined; @@ -1050,18 +1069,7 @@ export class SettingsEditor2 extends EditorPane { target: reportedTarget }; - /* __GDPR__ - "settingsEditor.settingModified" : { - "key" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "The setting that is being modified." }, - "groupId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "Whether the setting is from the local search or remote search provider, if applicable." }, - "nlpIndex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The index of the setting in the remote search provider results, if applicable." }, - "displayIndex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The index of the setting in the combined search results, if applicable." }, - "showConfiguredOnly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "Whether the user is in the modified view, which shows configured settings only." }, - "isReset" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "Identifies whether a setting was reset to its default value." }, - "target" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "The scope of the setting, such as user or workspace." } - } - */ - this.telemetryService.publicLog('settingsEditor.settingModified', data); + this.telemetryService.publicLog2<SettingsEditorModifiedSettingEvent, SettingsEditorModifiedSettingClassification>('settingsEditor.settingModified', data); } private onSearchModeToggled(): void { @@ -1271,7 +1279,7 @@ export class SettingsEditor2 extends EditorPane { await this.triggerSearch(query.replace(/\u203A/g, ' ')); if (query && this.searchResultModel) { - this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(query, this.searchResultModel!.getUniqueResults())); + this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(this.searchResultModel!.getUniqueResults())); } } @@ -1360,7 +1368,20 @@ export class SettingsEditor2 extends EditorPane { return filterModel; } - private reportFilteringUsed(query: string, results: ISearchResult[]): void { + private reportFilteringUsed(results: ISearchResult[]): void { + type SettingsEditorFilterEvent = { + 'durations.nlpResult': number | undefined; + 'counts.nlpResult': number | undefined; + 'counts.filterResult': number | undefined; + 'requestCount': number | undefined; + }; + type SettingsEditorFilterClassification = { + 'durations.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'How long the remote search provider took, if applicable.' }; + 'counts.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'The number of matches found by the remote search provider, if applicable.' }; + 'counts.filterResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'The number of matches found by the local search provider, if applicable.' }; + 'requestCount': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'The number of requests sent to Bing, if applicable.' }; + }; + const nlpResult = results[SearchResultIdx.Remote]; const nlpMetadata = nlpResult?.metadata; @@ -1382,20 +1403,13 @@ export class SettingsEditor2 extends EditorPane { const requestCount = nlpMetadata?.requestCount; const data = { - durations: duration, - counts, + 'durations.nlpResult': duration.nlpResult, + 'counts.nlpResult': counts['nlpResult'], + 'counts.filterResult': counts['filterResult'], requestCount }; - /* __GDPR__ - "settingsEditor.filter" : { - "durations.nlpResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "How long the remote search provider took, if applicable." }, - "counts.nlpResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The number of matches found by the remote search provider, if applicable." }, - "counts.filterResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The number of matches found by the local search provider, if applicable." }, - "requestCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The number of requests sent to Bing, if applicable." } - } - */ - this.telemetryService.publicLog('settingsEditor.filter', data); + this.telemetryService.publicLog2<SettingsEditorFilterEvent, SettingsEditorFilterClassification>('settingsEditor.filter', data); } private triggerFilterPreferences(query: string): Promise<void> { @@ -1520,15 +1534,17 @@ export class SettingsEditor2 extends EditorPane { if (isCancellationError(err)) { return Promise.reject(err); } else { - /* __GDPR__ - "settingsEditor.searchError" : { - "message": { "classification": "CallstackOrException", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "The error message of the search error." } - } - */ + type SettingsSearchErrorEvent = { + 'message': string; + }; + type SettingsSearchErrorClassification = { + 'message': { 'classification': 'CallstackOrException'; 'purpose': 'FeatureInsight'; 'owner': 'rzhao271'; 'comment': 'The error message of the search error.' }; + }; + const message = getErrorMessage(err).trim(); if (message && message !== 'Error') { // "Error" = any generic network error - this.telemetryService.publicLogError('settingsEditor.searchError', { message }); + this.telemetryService.publicLogError2<SettingsSearchErrorEvent, SettingsSearchErrorClassification>('settingsEditor.searchError', { message }); this.logService.info('Setting search error: ' + message); } return null; @@ -1545,7 +1561,7 @@ export class SettingsEditor2 extends EditorPane { // space it has, otherwise setViewVisible results in the first panel // showing up at the minimum size whenever the Settings editor // opens for the first time. - this.splitView.layout(this.bodyContainer.clientWidth); + this.splitView.layout(this.bodyContainer.clientWidth, listHeight); const firstViewWasVisible = this.splitView.isViewVisible(0); const firstViewVisible = this.bodyContainer.clientWidth >= SettingsEditor2.NARROW_TOTAL_WIDTH; diff --git a/src/vs/workbench/contrib/remote/browser/media/remoteViewlet.css b/src/vs/workbench/contrib/remote/browser/media/remoteViewlet.css index 243fc58a8a5..ef32cf43f39 100644 --- a/src/vs/workbench/contrib/remote/browser/media/remoteViewlet.css +++ b/src/vs/workbench/contrib/remote/browser/media/remoteViewlet.css @@ -36,25 +36,3 @@ .remote-help-content .monaco-list .monaco-list-row .monaco-tl-twistie { width: 0px !important; } - -.monaco-workbench .part > .title > .title-actions .switch-remote { - display: flex; - align-items: center; - font-size: 11px; - margin-right: 0.3em; - height: 20px; - flex-shrink: 1; -} - -.monaco-workbench.mac .part > .title > .title-actions .switch-remote { - border-radius: 4px; -} - -.switch-remote > .monaco-select-box { - border: none; - display: block; -} - -.monaco-workbench .part > .title > .title-actions .switch-remote > .monaco-select-box { - padding: 1px 22px 2px 6px; -} diff --git a/src/vs/workbench/contrib/remote/browser/remote.contribution.ts b/src/vs/workbench/contrib/remote/browser/remote.contribution.ts index 6982a033c25..63e7f2df0b4 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.contribution.ts @@ -17,7 +17,7 @@ workbenchContributionsRegistry.registerWorkbenchContribution(ShowCandidateContri workbenchContributionsRegistry.registerWorkbenchContribution(TunnelFactoryContribution, LifecyclePhase.Ready); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteStatusIndicator, LifecyclePhase.Starting); -workbenchContributionsRegistry.registerWorkbenchContribution(ForwardedPortsView, LifecyclePhase.Eventually); +workbenchContributionsRegistry.registerWorkbenchContribution(ForwardedPortsView, LifecyclePhase.Restored); workbenchContributionsRegistry.registerWorkbenchContribution(PortRestore, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(AutomaticPortForwarding, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteMarkers, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index e6b3c873d57..fae968c9cd6 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -101,7 +101,8 @@ export class TunnelViewModel implements ITunnelViewModel { id: TunnelPrivacyId.Private, themeIcon: privatePortIcon.id, label: nls.localize('tunnelPrivacy.private', "Private") - } + }, + strip: () => undefined }; constructor( @@ -415,6 +416,31 @@ class ActionBarRenderer extends Disposable implements ITableRenderer<ActionBarCe })); } + private tunnelContext(tunnel: ITunnelItem): ITunnelItem { + let context: ITunnelItem | undefined; + if (tunnel instanceof TunnelItem) { + context = tunnel.strip(); + } + if (!context) { + context = { + tunnelType: tunnel.tunnelType, + remoteHost: tunnel.remoteHost, + remotePort: tunnel.remotePort, + localAddress: tunnel.localAddress, + protocol: tunnel.protocol, + localUri: tunnel.localUri, + localPort: tunnel.localPort, + name: tunnel.name, + closeable: tunnel.closeable, + source: tunnel.source, + privacy: tunnel.privacy, + processDescription: tunnel.processDescription, + label: tunnel.label + }; + } + return context; + } + renderActionBarItem(element: ActionBarCell, templateData: IActionBarTemplateData): void { templateData.label.element.style.display = 'flex'; templateData.label.setLabel(element.label, undefined, @@ -424,7 +450,7 @@ class ActionBarRenderer extends Disposable implements ITableRenderer<ActionBarCe : element.tooltip, extraClasses: element.menuId === MenuId.TunnelLocalAddressInline ? ['ports-view-actionbar-cell-localaddress'] : undefined }); - templateData.actionBar.context = element.tunnel; + templateData.actionBar.context = this.tunnelContext(element.tunnel); templateData.container.style.paddingLeft = '10px'; const context: [string, any][] = [ @@ -566,6 +592,29 @@ class TunnelItem implements ITunnelItem { tunnelService); } + /** + * Removes all non-serializable properties from the tunnel + * @returns A new TunnelItem without any services + */ + public strip(): TunnelItem | undefined { + return new TunnelItem( + this.tunnelType, + this.remoteHost, + this.remotePort, + this.source, + this.hasRunningProcess, + this.protocol, + this.localUri, + this.localAddress, + this.localPort, + this.closeable, + this.name, + this.runningProcess, + this.pid, + this._privacy + ); + } + constructor( public tunnelType: TunnelType, public remoteHost: string, diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 055172e9e9b..600b2af4b21 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -1371,7 +1371,8 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor private setViewState(state: IViewState): void { this.viewState = state; this.stylesheet.textContent = ` - .monaco-editor .dirty-diff-modified,.monaco-editor .dirty-diff-added{border-left-width:${state.width}px;} + .monaco-editor .dirty-diff-modified { background-size: ${state.width}px 4.5px; } + .monaco-editor .dirty-diff-modified, .monaco-editor .dirty-diff-added{border-left-width:${state.width}px;} .monaco-editor .dirty-diff-modified, .monaco-editor .dirty-diff-added, .monaco-editor .dirty-diff-deleted { opacity: ${state.visibility === 'always' ? 1 : 0}; } @@ -1474,8 +1475,7 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = if (editorGutterModifiedBackgroundColor) { collector.addRule(` .monaco-editor .dirty-diff-modified { - background-size: 3px 4.5px; - background-repeat-x: no-repeat; + background-repeat: repeat-y; background-image: linear-gradient(${linearGradient}); transition: opacity 0.5s; } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewService.ts b/src/vs/workbench/contrib/scm/browser/scmViewService.ts index 330dab052b1..757c5526c7a 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewService.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewService.ts @@ -12,7 +12,7 @@ import { SCMMenus } from 'vs/workbench/contrib/scm/browser/menus'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { debounce } from 'vs/base/common/decorators'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { compareFileNames } from 'vs/base/common/comparers'; +import { compareFileNames, comparePaths } from 'vs/base/common/comparers'; import { basename } from 'vs/base/common/resources'; import { binarySearch } from 'vs/base/common/arrays'; @@ -131,7 +131,12 @@ export class SCMViewService implements ISCMViewService { const name1 = getRepositoryName(workspaceContextService, op1); const name2 = getRepositoryName(workspaceContextService, op2); - return compareFileNames(name1, name2); + const nameComparison = compareFileNames(name1, name2); + if (nameComparison === 0 && op1.provider.rootUri && op2.provider.rootUri) { + return comparePaths(op1.provider.rootUri.fsPath, op2.provider.rootUri.fsPath); + } + + return nameComparison; }; scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); @@ -275,9 +280,7 @@ export class SCMViewService implements ISCMViewService { private insertRepository(repositories: ISCMRepository[], repository: ISCMRepository): void { const index = binarySearch(repositories, repository, this._compareRepositories); - if (index < 0) { - repositories.splice(~index, 0, repository); - } + repositories.splice(index < 0 ? ~index : index, 0, repository); } private onWillSaveState(): void { diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index b073fc13e0d..aa6e96d9afe 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -83,7 +83,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { snippet: for (const snippet of snippets) { - if (context.triggerKind === CompletionTriggerKind.TriggerCharacter && !snippet.prefixLow.includes(triggerCharacterLow)) { + if (context.triggerKind === CompletionTriggerKind.TriggerCharacter && !snippet.prefixLow.startsWith(triggerCharacterLow)) { // strict -> when having trigger characters they must prefix-match continue snippet; } diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index 8f9616dbd3b..540e3fce155 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -11,7 +11,6 @@ import { LanguageService } from 'vs/editor/common/services/languageService'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution'; import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { CompletionContext, CompletionItemLabel, CompletionItemRanges, CompletionTriggerKind } from 'vs/editor/common/languages'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; @@ -403,7 +402,8 @@ suite('SnippetsService', function () { }); test('issue #61296: VS code freezes when editing CSS file with emoji', async function () { - disposableStore.add(LanguageConfigurationRegistry.register('fooLang'!, { + const languageConfigurationService = new TestLanguageConfigurationService(); + disposableStore.add(languageConfigurationService.register('fooLang', { wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g })); @@ -417,7 +417,7 @@ suite('SnippetsService', function () { SnippetSource.User )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, languageConfigurationService); let model = disposables.add(createTextModel('.🐷-a-b', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 8), context)!; @@ -548,7 +548,8 @@ suite('SnippetsService', function () { }); test('Snippet will replace auto-closing pair if specified in prefix', async function () { - disposableStore.add(LanguageConfigurationRegistry.register('fooLang'!, { + const languageConfigurationService = new TestLanguageConfigurationService(); + disposableStore.add(languageConfigurationService.register('fooLang', { brackets: [ ['{', '}'], ['[', ']'], @@ -566,7 +567,7 @@ suite('SnippetsService', function () { SnippetSource.User )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, languageConfigurationService); let model = createTextModel('[psc]', 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 5), context)!; @@ -728,7 +729,7 @@ suite('SnippetsService', function () { model.dispose(); }); - test('Snippets disappear with . key #145960', async function () { + test.skip('Snippets disappear with . key #145960', async function () { snippetService = new SimpleSnippetService([ new Snippet(['fooLang'], 'div', 'div', '', 'div', '', SnippetSource.User), new Snippet(['fooLang'], 'div.', 'div.', '', 'div.', '', SnippetSource.User), diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts index 22598d4aba7..043a8472b5e 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts @@ -148,7 +148,8 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { if (result) { const { uri, isDirectory } = result; const linkToOpen = { - text: matchLink, + // Use the absolute URI's path here so the optional line/col get detected + text: result.uri.fsPath + (matchLink.match(/:\d+(:\d+)?$/)?.[0] || ''), uri, bufferRange: link.bufferRange, type: link.type @@ -194,7 +195,7 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { } interface IResourceMatch { - uri: URI | undefined; + uri: URI; isDirectory?: boolean; } @@ -209,7 +210,9 @@ export class TerminalUrlLinkOpener implements ITerminalLinkOpener { if (!link.uri) { throw new Error('Tried to open a url without a resolved URI'); } - this._openerService.open(link.uri || URI.parse(link.text), { + // It's important to use the raw string value here to avoid converting pre-encoded values + // from the URL like `%2B` -> `+`. + this._openerService.open(link.text, { allowTunneling: this._isRemote, allowContributedOpeners: true, }); diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalShellIntegrationLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalShellIntegrationLinkDetector.ts index 15993914e93..c43e00e3f62 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalShellIntegrationLinkDetector.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalShellIntegrationLinkDetector.ts @@ -51,6 +51,9 @@ export class TerminalShellIntegrationLinkDetector implements ITerminalLinkDetect } private _matches(lines: IBufferLine[]): boolean { + if (lines.length < linkCodes.length) { + return false; + } let cell: IBufferCell | undefined; for (let i = 0; i < linkCodes.length; i++) { cell = lines[Math.floor(i / this.xterm.cols)].getCell(i % this.xterm.cols, cell); diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index 2c6dcf55773..5b7471bb284 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -27,7 +27,6 @@ if [[ "$PROMPT_COMMAND" =~ .*(' '.*\;)|(\;.*' ').* ]]; then fi if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then - echo -e "\033[1;33mShell integration was disabled by the shell\033[0m" return fi diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index af4b8524493..02867a99753 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -81,12 +81,26 @@ z-index: 31; } -.monaco-workbench .simple-find-part-wrapper { +.xterm .xterm-screen { + cursor: text; +} + +.monaco-workbench .simple-find-part-wrapper.result-count { z-index: 33 !important; + padding-right: 80px; } -.xterm .xterm-screen { - cursor: text; +.result-count .simple-find-part { + width: 280px; + max-width: 280px; + min-width: 280px; +} + +.result-count .matchesCount { + width: 68px; + max-width: 68px; + min-width: 68px; + padding-left: 5px; } .xterm .xterm-accessibility { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 27ec4f11389..f8aae95de73 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -9,12 +9,14 @@ import 'vs/css!./media/terminal'; import 'vs/css!./media/widgets'; import 'vs/css!./media/xterm'; import * as nls from 'vs/nls'; +import { URI } from 'vs/base/common/uri'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight, KeybindingsRegistry, IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; import { Extensions as ViewContainerExtensions, IViewContainersRegistry, ViewContainerLocation, IViewsRegistry } from 'vs/workbench/common/views'; +import { Extensions as DragAndDropExtensions, IDragAndDropContributionRegistry, IDraggedResourceEditorInput } from 'vs/workbench/browser/dnd'; import { registerTerminalActions, terminalSendSequenceCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; import { TERMINAL_VIEW_ID, TerminalCommandId, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -22,7 +24,7 @@ import { registerColors } from 'vs/workbench/contrib/terminal/common/terminalCol import { setupTerminalCommands } from 'vs/workbench/contrib/terminal/browser/terminalCommands'; import { TerminalService } from 'vs/workbench/contrib/terminal/browser/terminalService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ITerminalEditorService, ITerminalGroupService, ITerminalInstanceService, ITerminalService, terminalEditorId } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalEditorService, ITerminalGroupService, ITerminalInstanceService, ITerminalService, TerminalDataTransfers, terminalEditorId } from 'vs/workbench/contrib/terminal/browser/terminal'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; @@ -49,6 +51,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { RemoteTerminalBackendContribution } from 'vs/workbench/contrib/terminal/browser/remoteTerminalBackend'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { TerminalMainContribution } from 'vs/workbench/contrib/terminal/browser/terminalMainContribution'; +import { Schemas } from 'vs/base/common/network'; // Register services registerSingleton(ITerminalService, TerminalService, true); @@ -81,6 +84,7 @@ workbenchRegistry.registerWorkbenchContribution(RemoteTerminalBackendContributio registerTerminalPlatformConfiguration(); registerTerminalConfiguration(); +// Register editor/dnd contributions Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).registerEditorSerializer(TerminalEditorInput.ID, TerminalInputSerializer); Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( @@ -92,6 +96,27 @@ Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane new SyncDescriptor(TerminalEditorInput) ] ); +Registry.as<IDragAndDropContributionRegistry>(DragAndDropExtensions.DragAndDropContribution).register({ + dataFormatKey: TerminalDataTransfers.Terminals, + getEditorInputs(data) { + const editors: IDraggedResourceEditorInput[] = []; + try { + const terminalEditors: string[] = JSON.parse(data); + for (const terminalEditor of terminalEditors) { + editors.push({ resource: URI.parse(terminalEditor) }); + } + } catch (error) { + // Invalid transfer + } + return editors; + }, + setData(resources, event) { + const terminalResources = resources.filter(({ resource }) => resource.scheme === Schemas.vscodeTerminal); + if (terminalResources.length) { + event.dataTransfer?.setData(TerminalDataTransfers.Terminals, JSON.stringify(terminalResources.map(({ resource }) => resource.toString()))); + } + } +}); // Register views const VIEW_CONTAINER = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 37ac145ece8..c48979fa4d6 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -15,7 +15,7 @@ import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { IEditableData } from 'vs/workbench/common/views'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IKeyMods } from 'vs/platform/quickinput/common/quickInput'; -import { ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { ITerminalCapabilityStore, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; @@ -146,6 +146,7 @@ export interface ITerminalService extends ITerminalInstanceHost { onDidInputInstanceData: Event<ITerminalInstance>; onDidRegisterProcessSupport: Event<void>; onDidChangeConnectionState: Event<void>; + onDidRequestHideFindWidget: Event<void>; /** * Creates a terminal. @@ -554,6 +555,8 @@ export interface ITerminalInstance { */ onExit: Event<number | ITerminalLaunchError | undefined>; + onDidChangeFindResults: Event<{ resultIndex: number; resultCount: number } | undefined>; + readonly exitCode: number | undefined; readonly areLinksReady: boolean; @@ -649,7 +652,7 @@ export interface ITerminalInstance { /** * Copies the terminal selection to the clipboard. */ - copySelection(asHtml?: boolean): Promise<void>; + copySelection(asHtml?: boolean, command?: ITerminalCommand): Promise<void>; /** * Current selection in the terminal. @@ -864,6 +867,8 @@ export interface IXtermTerminal { */ target?: TerminalLocation; + findResult?: { resultIndex: number; resultCount: number }; + /** * Find the next instance of the term */ @@ -918,3 +923,7 @@ export const enum LinuxDistro { Fedora = 2, Ubuntu = 3, } + +export const enum TerminalDataTransfers { + Terminals = 'Terminals' +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 9fa3c648490..71eeff2a7ac 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -75,6 +75,7 @@ export class TerminalEditor extends EditorPane { this._findWidget = instantiationService.createInstance(TerminalFindWidget, this._findState); this._dropdownMenu = this._register(menuService.createMenu(MenuId.TerminalNewDropdownContext, _contextKeyService)); this._instanceMenu = this._register(menuService.createMenu(MenuId.TerminalEditorInstanceContext, _contextKeyService)); + this._register(this._terminalService.onDidRequestHideFindWidget(() => this.hideFindWidget())); } override async setInput(newInput: TerminalEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken) { @@ -91,6 +92,7 @@ export class TerminalEditor extends EditorPane { // panel and the editors, this is needed so that the active instance gets set // when focus changes between them. this._register(this._editorInput.terminalInstance.onDidFocus(() => this._setActiveInstance())); + this._register(this._editorInput.terminalInstance.onDidChangeFindResults(() => this._findWidget.updateResultCount())); this._editorInput.setCopyLaunchConfig(this._editorInput.terminalInstance.shellLaunchConfig); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts index e4cda779a2b..6168afff93b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts @@ -7,8 +7,9 @@ import { SimpleFindWidget } from 'vs/workbench/contrib/codeEditor/browser/find/s import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; -import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; export class TerminalFindWidget extends SimpleFindWidget { protected _findInputFocused: IContextKey<boolean>; @@ -19,9 +20,11 @@ export class TerminalFindWidget extends SimpleFindWidget { findState: FindReplaceState, @IContextViewService _contextViewService: IContextViewService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @ITerminalService private readonly _terminalService: ITerminalService + @ITerminalService private readonly _terminalService: ITerminalService, + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService ) { - super(_contextViewService, _contextKeyService, findState, true); + super(_contextViewService, _contextKeyService, findState, { showOptionButtons: true, showResultCount: true }); + this._register(findState.onFindReplaceStateChange(() => { this.show(); })); @@ -68,7 +71,24 @@ export class TerminalFindWidget extends SimpleFindWidget { if (instance) { instance.focus(); } - instance?.xterm?.clearSearchDecorations(); + // Terminals in a group currently share a find widget, so hide + // all decorations for terminals in this group + const activeGroup = this._terminalGroupService.activeGroup; + if (instance?.target !== TerminalLocation.Editor && activeGroup) { + for (const terminal of activeGroup.terminalInstances) { + terminal.xterm?.clearSearchDecorations(); + } + } else { + instance?.xterm?.clearSearchDecorations(); + } + } + + protected async _getResultCount(): Promise<{ resultIndex: number; resultCount: number } | undefined> { + const instance = this._terminalService.activeInstance; + if (instance) { + return instance.xterm?.findResult; + } + return undefined; } protected _onInputChanged() { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 4ffe4542f7c..18be1c0ae4a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -52,7 +52,7 @@ import { CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; import { TerminalLinkQuickpick } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick'; -import { IRequestAddInstanceToGroupEvent, ITerminalExternalLinkProvider, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRequestAddInstanceToGroupEvent, ITerminalExternalLinkProvider, ITerminalInstance, TerminalDataTransfers } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalLaunchHelpAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; @@ -329,6 +329,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { readonly onRequestAddInstanceToGroup = this._onRequestAddInstanceToGroup.event; private readonly _onDidChangeHasChildProcesses = this._register(new Emitter<boolean>()); readonly onDidChangeHasChildProcesses = this._onDidChangeHasChildProcesses.event; + private readonly _onDidChangeFindResults = new Emitter<{ resultIndex: number; resultCount: number } | undefined>(); + readonly onDidChangeFindResults = this._onDidChangeFindResults.event; constructor( private readonly _terminalFocusContextKey: IContextKey<boolean>, @@ -444,10 +446,15 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._xtermReadyPromise.then(async () => { // Wait for a period to allow a container to be ready await this._containerReadyBarrier.wait(); - if (this._configHelper.config.shellIntegration?.enabled && !this.shellLaunchConfig.executable) { + + // Resolve the executable ahead of time if shell integration is enabled, this should not + // be done for custom PTYs as that would cause extension Pseudoterminal-based terminals + // to hang in resolver extensions + if (!this.shellLaunchConfig.customPtyImplementation && this._configHelper.config.shellIntegration?.enabled && !this.shellLaunchConfig.executable) { const os = await this._processManager.getBackendOS(); this.shellLaunchConfig.executable = (await this._terminalProfileResolverService.getDefaultProfile({ remoteAuthority: this.remoteAuthority, os })).path; } + await this._createProcess(); // Re-establish the title after reconnect @@ -647,7 +654,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const lineDataEventAddon = new LineDataEventAddon(); this.xterm.raw.loadAddon(lineDataEventAddon); this.updateAccessibilitySupport(); - this.xterm.onDidRequestRunCommand(command => this.sendText(command, true)); + this.xterm.onDidRequestRunCommand(e => { + if (e.copyAsHtml) { + this.copySelection(true, e.command); + } else { + this.sendText(e.command.command, true); + } + }); // Write initial text, deferring onLineFeed listener when applicable to avoid firing // onLineData events containing initialText if (this._shellLaunchConfig.initialText) { @@ -975,6 +988,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const screenElement = xterm.attachToElement(xtermElement); + xterm.onDidChangeFindResults((results) => this._onDidChangeFindResults.fire(results)); + if (!xterm.raw.element || !xterm.raw.textarea) { throw new Error('xterm elements not set after open'); } @@ -1136,13 +1151,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return this.xterm ? this.xterm.raw.hasSelection() : false; } - async copySelection(asHtml?: boolean): Promise<void> { + async copySelection(asHtml?: boolean, command?: ITerminalCommand): Promise<void> { const xterm = await this._xtermReadyPromise; - if (this.hasSelection()) { + if (this.hasSelection() || (asHtml && command)) { if (asHtml) { - const selectionAsHtml = await xterm.getSelectionAsHtml(); + const textAsHtml = await xterm.getSelectionAsHtml(command); function listener(e: any) { - e.clipboardData.setData('text/html', selectionAsHtml); + e.clipboardData.setData('text/html', textAsHtml); e.preventDefault(); } document.addEventListener('copy', listener); @@ -1737,7 +1752,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { @debounce(2000) private async _updateProcessCwd(): Promise<void> { - if (this._isDisposed) { + if (this._isDisposed || this.shellLaunchConfig.customPtyImplementation) { return; } // reset cwd if it has changed, so file based url paths can be resolved @@ -2282,7 +2297,7 @@ class TerminalInstanceDragAndDropController extends Disposable implements dom.ID } onDragEnter(e: DragEvent) { - if (!containsDragType(e, DataTransfers.FILES, DataTransfers.RESOURCES, DataTransfers.TERMINALS, CodeDataTransfers.FILES)) { + if (!containsDragType(e, DataTransfers.FILES, DataTransfers.RESOURCES, TerminalDataTransfers.Terminals, CodeDataTransfers.FILES)) { return; } @@ -2292,7 +2307,7 @@ class TerminalInstanceDragAndDropController extends Disposable implements dom.ID } // Dragging terminals - if (containsDragType(e, DataTransfers.TERMINALS)) { + if (containsDragType(e, TerminalDataTransfers.Terminals)) { const side = this._getDropSide(e); this._dropOverlay.classList.toggle('drop-before', side === 'before'); this._dropOverlay.classList.toggle('drop-after', side === 'after'); @@ -2316,7 +2331,7 @@ class TerminalInstanceDragAndDropController extends Disposable implements dom.ID } // Dragging terminals - if (containsDragType(e, DataTransfers.TERMINALS)) { + if (containsDragType(e, TerminalDataTransfers.Terminals)) { const side = this._getDropSide(e); this._dropOverlay.classList.toggle('drop-before', side === 'before'); this._dropOverlay.classList.toggle('drop-after', side === 'after'); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts b/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts index 873f4e0ea91..0fe61abdc4d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts @@ -95,6 +95,7 @@ export class TerminalQuickAccessProvider extends PickerQuickAccessProvider<IPick if (highlights) { return { label, + description: terminal.description, highlights: { label: highlights }, buttons: [ { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 31995a09e77..8402e0e5362 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -141,6 +141,8 @@ export class TerminalService implements ITerminalService { get onDidRegisterProcessSupport(): Event<void> { return this._onDidRegisterProcessSupport.event; } private readonly _onDidChangeConnectionState = new Emitter<void>(); get onDidChangeConnectionState(): Event<void> { return this._onDidChangeConnectionState.event; } + private readonly _onDidRequestHideFindWidget = new Emitter<void>(); + get onDidRequestHideFindWidget(): Event<void> { return this._onDidRequestHideFindWidget.event; } constructor( @IContextKeyService private _contextKeyService: IContextKeyService, @@ -720,6 +722,7 @@ export class TerminalService implements ITerminalService { } sourceGroup.removeInstance(source); this._terminalEditorService.openEditor(source); + this._onDidRequestHideFindWidget.fire(); } async moveToTerminalView(source?: ITerminalInstance, target?: ITerminalInstance, side?: 'before' | 'after'): Promise<void> { @@ -766,6 +769,7 @@ export class TerminalService implements ITerminalService { this._onDidChangeInstances.fire(); this._onDidChangeActiveGroup.fire(this._terminalGroupService.activeGroup); this._terminalGroupService.showPanel(true); + this._onDidRequestHideFindWidget.fire(); } protected _initInstanceListeners(instance: ITerminalInstance): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index c20833c2e64..eec5a502976 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -133,6 +133,7 @@ export class TerminalTabbedView extends Disposable { this._register(this._terminalGroupService.onDidChangeInstances(() => this._refreshShowTabs())); this._register(this._terminalGroupService.onDidChangeGroups(() => this._refreshShowTabs())); this._register(this._themeService.onDidColorThemeChange(theme => this._updateTheme(theme))); + this._register(this._terminalService.onDidRequestHideFindWidget(() => this.hideFindWidget())); this._updateTheme(); this._findWidget.focusTracker.onDidFocus(() => this._terminalContainer.classList.add(CssClass.FindFocus)); @@ -150,7 +151,11 @@ export class TerminalTabbedView extends Disposable { }); this._splitView = new SplitView(parentElement, { orientation: Orientation.HORIZONTAL, proportionalLayout: false }); - + this._terminalService.onDidCreateInstance(instance => { + instance.onDidChangeFindResults(() => { + this._findWidget.updateResultCount(); + }); + }); this._setupSplitView(terminalOuterContainer); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index b7a57b05460..5cf17115844 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -9,7 +9,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ITerminalGroupService, ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalGroupService, ITerminalInstance, ITerminalService, TerminalDataTransfers } from 'vs/workbench/contrib/terminal/browser/terminal'; import { localize } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -585,13 +585,13 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop<ITerminalInstance> { // Attach terminals type to event const terminals: ITerminalInstance[] = dndData.filter(e => 'instanceId' in (e as any)); if (terminals.length > 0) { - originalEvent.dataTransfer.setData(DataTransfers.TERMINALS, JSON.stringify(terminals.map(e => e.resource.toString()))); + originalEvent.dataTransfer.setData(TerminalDataTransfers.Terminals, JSON.stringify(terminals.map(e => e.resource.toString()))); } } onDragOver(data: IDragAndDropData, targetInstance: ITerminalInstance | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction { if (data instanceof NativeDragAndDropData) { - if (!containsDragType(originalEvent, DataTransfers.FILES, DataTransfers.RESOURCES, DataTransfers.TERMINALS, CodeDataTransfers.FILES)) { + if (!containsDragType(originalEvent, DataTransfers.FILES, DataTransfers.RESOURCES, TerminalDataTransfers.Terminals, CodeDataTransfers.FILES)) { return false; } } @@ -602,7 +602,7 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop<ITerminalInstance> { this._autoFocusInstance = targetInstance; } - if (!targetInstance && !containsDragType(originalEvent, DataTransfers.TERMINALS)) { + if (!targetInstance && !containsDragType(originalEvent, TerminalDataTransfers.Terminals)) { return data instanceof ElementsDragAndDropData; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalUri.ts b/src/vs/workbench/contrib/terminal/browser/terminalUri.ts index eb44e80d2f1..9a04a07b3c2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalUri.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalUri.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DataTransfers } from 'vs/base/browser/dnd'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; -import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalInstance, TerminalDataTransfers } from 'vs/workbench/contrib/terminal/browser/terminal'; export function parseTerminalUri(resource: URI): ITerminalIdentifier { const [, workspaceId, instanceId] = resource.path.split('/'); @@ -34,7 +33,7 @@ export interface IPartialDragEvent { } export function getTerminalResourcesFromDragEvent(event: IPartialDragEvent): URI[] | undefined { - const resources = event.dataTransfer?.getData(DataTransfers.TERMINALS); + const resources = event.dataTransfer?.getData(TerminalDataTransfers.Terminals); if (resources) { const json = JSON.parse(resources); const result = []; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 03f433bd72b..03cb3ead663 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -14,7 +14,7 @@ import { IThemeService, IColorTheme, registerThemingParticipant, ICssStyleCollec import { switchTerminalActionViewItemSeparator, switchTerminalShowTabsTitle } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR, TERMINAL_DRAG_AND_DROP_BACKGROUND, TERMINAL_TAB_ACTIVE_BORDER } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; -import { ICreateTerminalOptions, ITerminalGroupService, ITerminalInstance, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ICreateTerminalOptions, ITerminalGroupService, ITerminalInstance, ITerminalService, TerminalConnectionState, TerminalDataTransfers } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -42,7 +42,6 @@ import { ColorScheme } from 'vs/platform/theme/common/theme'; import { getColorClass, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { DataTransfers } from 'vs/base/browser/dnd'; import { getTerminalActionBarArgs } from 'vs/workbench/contrib/terminal/browser/terminalMenus'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { getShellIntegrationTooltip } from 'vs/workbench/contrib/terminal/browser/terminalTooltip'; @@ -449,7 +448,7 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { this._elementDisposables.push(dom.addDisposableListener(this.element, dom.EventType.DRAG_START, e => { const instance = this._terminalGroupService.activeInstance; if (e.dataTransfer && instance) { - e.dataTransfer.setData(DataTransfers.TERMINALS, JSON.stringify([instance.resource.toString()])); + e.dataTransfer.setData(TerminalDataTransfers.Terminals, JSON.stringify([instance.resource.toString()])); } })); } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index 678d1c5164a..40b75ff1352 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -49,7 +49,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { private _decorations: Map<number, IDisposableDecoration> = new Map(); private _placeholderDecoration: IDecoration | undefined; - private readonly _onDidRequestRunCommand = this._register(new Emitter<string>()); + private readonly _onDidRequestRunCommand = this._register(new Emitter<{ command: ITerminalCommand; copyAsHtml?: boolean }>()); readonly onDidRequestRunCommand = this._onDidRequestRunCommand.event; constructor( @@ -313,10 +313,14 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { class: 'copy-output', tooltip: 'Copy Output', dispose: () => { }, id: 'terminal.copyOutput', label: localize("terminal.copyOutput", 'Copy Output'), enabled: true, run: () => this._clipboardService.writeText(command.getOutput()!) }); + actions.push({ + class: 'copy-output', tooltip: 'Copy Output as HTML', dispose: () => { }, id: 'terminal.copyOutputAsHtml', label: localize("terminal.copyOutputAsHtml", 'Copy Output as HTML'), enabled: true, + run: () => this._onDidRequestRunCommand.fire({ command, copyAsHtml: true }) + }); } actions.push({ class: 'rerun-command', tooltip: 'Rerun Command', dispose: () => { }, id: 'terminal.rerunCommand', label: localize("terminal.rerunCommand", 'Rerun Command'), enabled: true, - run: () => this._onDidRequestRunCommand.fire(command.command) + run: () => this._onDidRequestRunCommand.fire({ command }) }); return actions; } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 97171bea09d..30b72f90a8e 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -32,7 +32,7 @@ import { Color } from 'vs/base/common/color'; import { ShellIntegrationAddon } from 'vs/platform/terminal/common/xterm/shellIntegrationAddon'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DecorationAddon } from 'vs/workbench/contrib/terminal/browser/xterm/decorationAddon'; -import { ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { ITerminalCapabilityStore, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; import { Emitter } from 'vs/base/common/event'; // How long in milliseconds should an average frame take to render for a notification to appear @@ -68,9 +68,15 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { private _webglAddon?: WebglAddonType; private _serializeAddon?: SerializeAddonType; - private readonly _onDidRequestRunCommand = new Emitter<string>(); + private _lastFindResult: { resultIndex: number; resultCount: number } | undefined; + get findResult(): { resultIndex: number; resultCount: number } | undefined { return this._lastFindResult; } + + private readonly _onDidRequestRunCommand = new Emitter<{ command: ITerminalCommand; copyAsHtml?: boolean }>(); readonly onDidRequestRunCommand = this._onDidRequestRunCommand.event; + private readonly _onDidChangeFindResults = new Emitter<{ resultIndex: number; resultCount: number } | undefined>(); + readonly onDidChangeFindResults = this._onDidChangeFindResults.event; + get commandTracker(): ICommandTracker { return this._commandNavigationAddon; } get shellIntegration(): IShellIntegration { return this._shellIntegrationAddon; } @@ -169,17 +175,29 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { } private _createDecorationAddon(): void { this._decorationAddon = this._instantiationService.createInstance(DecorationAddon, this._capabilities); - this._decorationAddon.onDidRequestRunCommand(command => this._onDidRequestRunCommand.fire(command)); + this._decorationAddon.onDidRequestRunCommand(e => this._onDidRequestRunCommand.fire(e)); this.raw.loadAddon(this._decorationAddon); } - async getSelectionAsHtml(): Promise<string> { + async getSelectionAsHtml(command?: ITerminalCommand): Promise<string> { if (!this._serializeAddon) { const Addon = await this._getSerializeAddonConstructor(); this._serializeAddon = new Addon(); this.raw.loadAddon(this._serializeAddon); } - return this._serializeAddon.serializeAsHTML({ onlySelection: true }); + if (command) { + const length = command.getOutput()?.length; + const row = command.marker?.line; + if (!length || !row) { + throw new Error(`No row ${row} or output length ${length} for command ${command}`); + } + await this.raw.select(0, row + 1, length - Math.floor(length / this.raw.cols)); + } + const result = this._serializeAddon.serializeAsHTML({ onlySelection: true }); + if (command) { + this.raw.clearSelection(); + } + return result; } attachToElement(container: HTMLElement): HTMLElement { @@ -292,6 +310,10 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { const AddonCtor = await this._getSearchAddonConstructor(); this._searchAddon = new AddonCtor(); this.raw.loadAddon(this._searchAddon); + this._searchAddon.onDidChangeResults((results: { resultIndex: number; resultCount: number } | undefined) => { + this._lastFindResult = results; + this._onDidChangeFindResults.fire(results); + }); return this._searchAddon; } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 2422169e9b5..1d3a1aa85db 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -163,7 +163,7 @@ class TerminalBackendRegistry implements ITerminalBackendRegistry { private readonly _backends = new Map<string, ITerminalBackend>(); registerTerminalBackend(backend: ITerminalBackend): void { - const key = backend.remoteAuthority ?? ''; + const key = this._sanitizeRemoteAuthority(backend.remoteAuthority); if (this._backends.has(key)) { throw new Error(`A terminal backend with remote authority '${key}' was already registered.`); } @@ -171,7 +171,12 @@ class TerminalBackendRegistry implements ITerminalBackendRegistry { } getTerminalBackend(remoteAuthority: string | undefined): ITerminalBackend | undefined { - return this._backends.get(remoteAuthority ?? ''); + return this._backends.get(this._sanitizeRemoteAuthority(remoteAuthority)); + } + + private _sanitizeRemoteAuthority(remoteAuthority: string | undefined) { + // Normalize the key to lowercase as the authority is case-insensitive + return remoteAuthority?.toLowerCase() ?? ''; } } Registry.add(TerminalExtensions.Backend, new TerminalBackendRegistry()); diff --git a/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts b/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts index 55ffbaefd99..59ba5731ce5 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts @@ -27,7 +27,7 @@ export const TERMINAL_SELECTION_BACKGROUND_COLOR = registerColor('terminal.selec light: '#00000040', dark: '#FFFFFF40', hcDark: '#FFFFFF80', - hcLight: '#F2F2F2' + hcLight: '#0F4A85' }, nls.localize('terminal.selectionBackground', 'The selection background color of the terminal.')); export const TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR = registerColor('terminalCommandDecoration.defaultBackground', { light: '#00000040', diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts index 04324bfe4be..aabd2add651 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts @@ -14,7 +14,7 @@ import { ByLocationTestItemElement } from 'vs/workbench/contrib/testing/browser/ import { IActionableTestTreeElement, ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; import { IComputedStateAndDurationAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState'; -import { InternalTestItem, TestDiffOpType, TestItemExpandState, TestResultState, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, TestDiffOpType, TestItemExpandState, TestResultState, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; @@ -105,7 +105,6 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes // children and should trust whatever the result service gives us. const explicitComputed = item.children.size ? undefined : result.computedState; - item.retired = result.retired; item.ownState = result.ownComputedState; item.ownDuration = result.ownDuration; @@ -271,7 +270,6 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes const prevState = this.results.getStateById(treeElement.test.item.extId)?.[1]; if (prevState) { - treeElement.retired = prevState.retired; treeElement.ownState = prevState.computedState; treeElement.ownDuration = prevState.ownDuration; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts index e7026fbfac6..cd34647d6d0 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts @@ -9,7 +9,7 @@ import { flatTestItemDelimiter } from 'vs/workbench/contrib/testing/browser/expl import { HierarchicalByLocationProjection as HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; import { ByLocationTestItemElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; import { NodeRenderDirective } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; -import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts index 3634813c32b..48e0b19c426 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; -import { applyTestItemUpdate, InternalTestItem, ITestItemUpdate } from 'vs/workbench/contrib/testing/common/testCollection'; +import { applyTestItemUpdate, InternalTestItem, ITestItemUpdate } from 'vs/workbench/contrib/testing/common/testTypes'; /** * Test tree element element that groups be hierarchy. diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts index 93868e0df2c..b4fa7d0aad1 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts @@ -10,7 +10,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; import { IDisposable } from 'vs/base/common/lifecycle'; import { MarshalledId } from 'vs/base/common/marshallingIds'; -import { InternalTestItem, ITestItemContext, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, ITestItemContext, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; /** * Describes a rendering of tests in the explorer view. Different @@ -120,11 +120,6 @@ export class TestItemTreeElement implements IActionableTestTreeElement { } /** - * Whether the node's test result is 'retired' -- from an outdated test run. - */ - public retired = false; - - /** * @inheritdoc */ public state = TestResultState.Unset; @@ -163,11 +158,11 @@ export class TestItemTreeElement implements IActionableTestTreeElement { const context: ITestItemContext = { $mid: MarshalledId.TestItemContext, - tests: [this.test], + tests: [InternalTestItem.serialize(this.test)], }; for (let p = this.parent; p && p.depth > 0; p = p.parent) { - context.tests.unshift(p.test); + context.tests.unshift(InternalTestItem.serialize(p.test)); } return context; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay.ts index 014af9cc1d2..0a1af60895c 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testTypes'; import { capabilityContextKeys } from 'vs/workbench/contrib/testing/common/testProfileService'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; diff --git a/src/vs/workbench/contrib/testing/browser/icons.ts b/src/vs/workbench/contrib/testing/browser/icons.ts index c469bd8c448..d4a2e945add 100644 --- a/src/vs/workbench/contrib/testing/browser/icons.ts +++ b/src/vs/workbench/contrib/testing/browser/icons.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { registerIcon, spinningLoading } from 'vs/platform/theme/common/iconRegistry'; import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { testingColorRunAction, testStatesToIconColors } from 'vs/workbench/contrib/testing/browser/theme'; -import { TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; export const testingViewIcon = registerIcon('test-view-icon', Codicon.beaker, localize('testViewIcon', 'View icon of the test view.')); export const testingRunIcon = registerIcon('testing-run-icon', Codicon.run, localize('testingRunIcon', 'Icon of the "run test" action.')); @@ -18,7 +18,6 @@ export const testingDebugAllIcon = registerIcon('testing-debug-all-icon', Codico export const testingDebugIcon = registerIcon('testing-debug-icon', Codicon.debugAltSmall, localize('testingDebugIcon', 'Icon of the "debug test" action.')); export const testingCancelIcon = registerIcon('testing-cancel-icon', Codicon.debugStop, localize('testingCancelIcon', 'Icon to cancel ongoing test runs.')); export const testingFilterIcon = registerIcon('testing-filter', Codicon.filter, localize('filterIcon', 'Icon for the \'Filter\' action in the testing view.')); -export const testingAutorunIcon = registerIcon('testing-autorun', Codicon.debugRerun, localize('autoRunIcon', 'Icon for the \'Autorun\' toggle in the testing view.')); export const testingHiddenIcon = registerIcon('testing-hidden', Codicon.eyeClosed, localize('hiddenIcon', 'Icon shown beside hidden tests, when they\'ve been shown.')); export const testingShowAsList = registerIcon('testing-show-as-list-icon', Codicon.listTree, localize('testingShowAsList', 'Icon shown when the test explorer is disabled as a tree.')); diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 7758973aa12..7400d289061 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -65,11 +65,6 @@ margin-right: 0.25em; } -.test-explorer .computed-state.retired, -.testing-run-glyph.retired { - opacity: 0.7 !important; -} - .test-explorer .test-is-hidden { opacity: 0.8; } diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 8230419e933..9382f47bc67 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -30,8 +30,7 @@ import * as icons from 'vs/workbench/contrib/testing/browser/icons'; import type { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService'; import { TestCommandId, TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { InternalTestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; -import { ITestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun'; +import { InternalTestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; @@ -650,47 +649,6 @@ export class GoToTest extends Action2 { } } -abstract class ToggleAutoRun extends Action2 { - - constructor(title: string, whenToggleIs: boolean) { - super({ - id: TestCommandId.ToggleAutoRun, - title, - icon: icons.testingAutorunIcon, - toggled: whenToggleIs === true ? ContextKeyExpr.true() : ContextKeyExpr.false(), - menu: { - id: MenuId.ViewTitle, - order: ActionOrder.AutoRun, - group: 'navigation', - when: ContextKeyExpr.and( - ContextKeyExpr.equals('view', Testing.ExplorerViewId), - TestingContextKeys.autoRun.isEqualTo(whenToggleIs) - ) - } - }); - } - - /** - * @override - */ - public run(accessor: ServicesAccessor) { - accessor.get(ITestingAutoRun).toggle(); - } -} - -export class AutoRunOnAction extends ToggleAutoRun { - constructor() { - super(localize('testing.turnOnAutoRun', "Turn On Auto Run"), false); - } -} - -export class AutoRunOffAction extends ToggleAutoRun { - constructor() { - super(localize('testing.turnOffAutoRun', "Turn Off Auto Run"), true); - } -} - - abstract class ExecuteTestAtCursor extends Action2 { constructor(options: IAction2Options, protected readonly group: TestRunProfileBitset) { super({ diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index d50f2b5279a..3605c75ec80 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -28,10 +28,9 @@ import { ITestingProgressUiService, TestingProgressTrigger, TestingProgressUiSer import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer'; import { testingConfiguation } from 'vs/workbench/contrib/testing/common/configuration'; import { TestCommandId, Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { ITestItem, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestItem, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestExplorerFilterState, TestExplorerFilterState } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; import { TestId, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; -import { ITestingAutoRun, TestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun'; import { TestingContentProvider } from 'vs/workbench/contrib/testing/common/testingContentProvider'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingDecorationsService } from 'vs/workbench/contrib/testing/common/testingDecorations'; @@ -50,7 +49,6 @@ registerSingleton(ITestResultStorage, TestResultStorage, true); registerSingleton(ITestProfileService, TestProfileService, true); registerSingleton(ITestResultService, TestResultService, true); registerSingleton(ITestExplorerFilterState, TestExplorerFilterState, true); -registerSingleton(ITestingAutoRun, TestingAutoRun, true); registerSingleton(ITestingOutputTerminalService, TestingOutputTerminalService, true); registerSingleton(ITestingPeekOpener, TestingPeekOpener, true); registerSingleton(ITestingProgressUiService, TestingProgressUiService, true); diff --git a/src/vs/workbench/contrib/testing/browser/testingConfigurationUi.ts b/src/vs/workbench/contrib/testing/browser/testingConfigurationUi.ts index 809868c283b..2427460910f 100644 --- a/src/vs/workbench/contrib/testing/browser/testingConfigurationUi.ts +++ b/src/vs/workbench/contrib/testing/browser/testingConfigurationUi.ts @@ -12,7 +12,7 @@ import { QuickPickInput, IQuickPickItem, IQuickInputService, IQuickPickItemButto import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { testingUpdateProfiles } from 'vs/workbench/contrib/testing/browser/icons'; import { testConfigurationGroupNames } from 'vs/workbench/contrib/testing/common/constants'; -import { InternalTestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { canUseProfileWithTest, ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; interface IConfigurationPickerOptions { diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 546db017969..5bdc3ee229a 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -40,7 +40,7 @@ import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/work import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme'; import { DefaultGutterClickAction, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { labelForTestInState, Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, ITestRunProfile, TestDiffOpType, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, ITestRunProfile, TestDiffOpType, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestDecoration as IPublicTestDecoration, ITestingDecorationsService, TestDecorations } from 'vs/workbench/contrib/testing/common/testingDecorations'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { isFailedState, maxPriority } from 'vs/workbench/contrib/testing/common/testingStates'; @@ -439,7 +439,6 @@ const createRunTestDecoration = (tests: readonly IncrementalTestCollectionItem[] let computedState = TestResultState.Unset; let hoverMessageParts: string[] = []; let testIdWithMessages: string | undefined; - let retired = false; for (let i = 0; i < tests.length; i++) { const test = tests[i]; const resultItem = states[i]; @@ -448,7 +447,6 @@ const createRunTestDecoration = (tests: readonly IncrementalTestCollectionItem[] hoverMessageParts.push(labelForTestInState(test.item.label, state)); } computedState = maxPriority(computedState, state); - retired = retired || !!resultItem?.retired; if (!testIdWithMessages && resultItem?.tasks.some(t => t.messages.length)) { testIdWithMessages = test.item.extId; } @@ -462,9 +460,6 @@ const createRunTestDecoration = (tests: readonly IncrementalTestCollectionItem[] let hoverMessage: IMarkdownString | undefined; let glyphMarginClassName = ThemeIcon.asClassName(icon) + ' testing-run-glyph'; - if (retired) { - glyphMarginClassName += ' retired'; - } return { range: firstLineRange(range), diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index 31d4e1eec73..c32661787e3 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -21,7 +21,7 @@ import { attachSuggestEnabledInputBoxStyler, ContextScopedSuggestEnabledInputWit import { testingFilterIcon } from 'vs/workbench/contrib/testing/browser/icons'; import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { denamespaceTestTag } from 'vs/workbench/contrib/testing/common/testCollection'; +import { denamespaceTestTag } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestExplorerFilterState, TestFilterTerm } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; @@ -78,7 +78,7 @@ export class TestingExplorerFilter extends BaseActionViewItem { const insertText = `@${ctrlId}:${tagId}`; return ({ label: `@${ctrlId}:${tagId}`, - detail: tag.ctrlLabel, + detail: this.testService.collection.getNodeById(ctrlId)?.item.label, insertText: tagId.includes(' ') ? `@${ctrlId}:"${tagId.replace(/(["\\])/g, '\\$1')}"` : insertText, }); }), @@ -205,6 +205,17 @@ class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { })), new Separator(), { + checked: this.filters.fuzzy.value, + class: undefined, + enabled: true, + id: 'fuzzy', + label: localize('testing.filters.fuzzyMatch', "Fuzzy Match"), + run: () => this.filters.fuzzy.value = !this.filters.fuzzy.value, + tooltip: '', + dispose: () => null + }, + new Separator(), + { checked: this.filters.isFilteringFor(TestFilterTerm.Hidden), class: undefined, enabled: this.testService.excluded.hasAny, diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 88ad930b4bd..b742efad054 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -56,9 +56,9 @@ import * as icons from 'vs/workbench/contrib/testing/browser/icons'; import { TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { ITestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; -import { labelForTestInState, TestCommandId, TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants'; +import { labelForTestInState, TestCommandId, TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { InternalTestItem, ITestRunProfile, TestItemExpandState, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, ITestRunProfile, TestItemExpandState, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestExplorerFilterState, TestExplorerFilterState, TestFilterTerm } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; @@ -513,6 +513,7 @@ export class TestingExplorerViewModel extends Disposable { this._register(Event.any( filterState.text.onDidChange, + filterState.fuzzy.onDidChange, testService.excluded.onTestExclusionsChanged, )(this.tree.refilter, this.tree)); @@ -911,13 +912,14 @@ class TestsFilter implements ITreeFilter<TestExplorerTreeElement> { return FilterResult.Include; } + const fuzzy = this.state.fuzzy.value; for (let e: TestItemTreeElement | null = element; e; e = e.parent) { // start as included if the first glob is a negation let included = this.state.globList[0].include === false ? FilterResult.Include : FilterResult.Inherit; const data = e.label.toLowerCase(); for (const { include, text } of this.state.globList) { - if (fuzzyContains(data, text)) { + if (fuzzy ? fuzzyContains(data, text) : data.includes(text)) { included = include ? FilterResult.Include : FilterResult.Exclude; } } @@ -1011,13 +1013,6 @@ const getLabelForTestTreeElement = (element: TestItemTreeElement) => { comment: ['{0} is the original label in testing.treeElementLabel, {1} is a duration'], }, '{0}, in {1}', label, formatDuration(element.duration)); } - - if (element.retired) { - label = localize({ - key: 'testing.treeElementLabelOutdated', - comment: ['{0} is the original label in testing.treeElementLabel'], - }, '{0}, outdated result', label, testStateNames[element.state]); - } } return label; @@ -1210,10 +1205,6 @@ class TestItemRenderer extends ActionableItemTemplateData<TestItemTreeElement> { : node.element.state); data.icon.className = 'computed-state ' + (icon ? ThemeIcon.asClassName(icon) : ''); - if (node.element.retired) { - data.icon.className += ' retired'; - } - label.resource = node.element.test.item.uri; options.title = getLabelForTestTreeElement(node.element); options.fileKind = FileKind.FILE; diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 67897613b3c..71b7930768c 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -65,7 +65,7 @@ import { AutoOpenPeekViewWhen, getTestingConfiguration, TestingConfigKeys } from import { Testing } from 'vs/workbench/contrib/testing/common/constants'; import { IObservableValue, MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { IRichLocation, ITestErrorMessage, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { IRichLocation, ITestErrorMessage, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; @@ -349,11 +349,20 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener * Gets the first failed message that can be displayed from the result. */ private getFailedCandidateMessage(test: TestResultItem) { - return mapFindTestMessage(test, (task, message, messageIndex, taskId) => - isFailedState(task.state) && message.location - ? { taskId, index: messageIndex, message } - : undefined - ); + let best: { taskId: number; index: number; message: ITestMessage } | undefined; + mapFindTestMessage(test, (task, message, messageIndex, taskId) => { + if (!isFailedState(task.state) || !message.location) { + return; + } + + if (best && message.type !== TestMessageType.Error) { + return; + } + + best = { taskId, index: messageIndex, message }; + }); + + return best; } } diff --git a/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts b/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts index 77ac8c24841..69b323858b3 100644 --- a/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts +++ b/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts @@ -13,7 +13,7 @@ import { ProgressLocation, UnmanagedProgress } from 'vs/platform/progress/common import { ViewContainerLocation } from 'vs/workbench/common/views'; import { AutoOpenTesting, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; import { LiveTestResult, TestResultItemChangeReason, TestStateCount } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; diff --git a/src/vs/workbench/contrib/testing/browser/theme.ts b/src/vs/workbench/contrib/testing/browser/theme.ts index 02c9577f04f..dcd6e2bb567 100644 --- a/src/vs/workbench/contrib/testing/browser/theme.ts +++ b/src/vs/workbench/contrib/testing/browser/theme.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { contrastBorder, editorErrorForeground, editorForeground, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ACTIVITY_BAR_BADGE_BACKGROUND } from 'vs/workbench/common/theme'; -import { TestMessageType, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestMessageType, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; export const testingColorIconFailed = registerColor('testing.iconFailed', { dark: '#f14c4c', diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index 44511be68c4..c8fc457be2d 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; export const enum Testing { // marked as "extension" so that any existing test extensions are assigned to it. diff --git a/src/vs/workbench/contrib/testing/common/getComputedState.ts b/src/vs/workbench/contrib/testing/common/getComputedState.ts index f3d87de8eff..74597ca30c3 100644 --- a/src/vs/workbench/contrib/testing/common/getComputedState.ts +++ b/src/vs/workbench/contrib/testing/common/getComputedState.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Iterable } from 'vs/base/common/iterator'; -import { TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; import { maxPriority, statePriority } from 'vs/workbench/contrib/testing/common/testingStates'; /** diff --git a/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts b/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts index 52ddb69be63..42e68e67209 100644 --- a/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts @@ -5,12 +5,11 @@ import { Emitter } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; -import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService'; export class MainThreadTestCollection extends AbstractIncrementalTestCollection<IncrementalTestCollectionItem> implements IMainThreadTestCollection { private busyProvidersChangeEmitter = new Emitter<number>(); - private retireTestEmitter = new Emitter<string>(); private expandPromises = new WeakMap<IncrementalTestCollectionItem, { pendingLvl: number; doneLvl: number; @@ -43,7 +42,6 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection< } public readonly onBusyProvidersChange = this.busyProvidersChangeEmitter.event; - public readonly onDidRetireTest = this.retireTestEmitter.event; constructor(private readonly expandActual: (id: string, levels: number) => Promise<void>) { super(); @@ -140,13 +138,6 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection< return { ...internal, children: new Set() }; } - /** - * @override - */ - protected override retireTest(testId: string) { - this.retireTestEmitter.fire(testId); - } - private *getIterator() { const queue = [this.rootIds]; while (queue.length) { diff --git a/src/vs/workbench/contrib/testing/common/testCoverage.ts b/src/vs/workbench/contrib/testing/common/testCoverage.ts index 7bed594032d..53030124dfc 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverage.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverage.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; -import { IFileCoverage, CoverageDetails, ICoveredCount } from 'vs/workbench/contrib/testing/common/testCollection'; +import { IFileCoverage, CoverageDetails, ICoveredCount } from 'vs/workbench/contrib/testing/common/testTypes'; export interface ICoverageAccessor { provideFileCoverage: (token: CancellationToken) => Promise<IFileCoverage[]>; diff --git a/src/vs/workbench/contrib/testing/common/testExclusions.ts b/src/vs/workbench/contrib/testing/common/testExclusions.ts index 1cc222a0e5f..8479e3b95d5 100644 --- a/src/vs/workbench/contrib/testing/common/testExclusions.ts +++ b/src/vs/workbench/contrib/testing/common/testExclusions.ts @@ -9,7 +9,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testTypes'; export class TestExclusions extends Disposable { private readonly excluded = this._register( diff --git a/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts b/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts index 2f5f78039ca..0be3c3dcc8d 100644 --- a/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts +++ b/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts @@ -5,8 +5,10 @@ import { Emitter, Event } from 'vs/base/common/event'; import { splitGlobAware } from 'vs/base/common/glob'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IObservableValue, MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; -import { namespaceTestTag } from 'vs/workbench/contrib/testing/common/testCollection'; +import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; +import { namespaceTestTag } from 'vs/workbench/contrib/testing/common/testTypes'; export interface ITestExplorerFilterState { _serviceBrand: undefined; @@ -36,6 +38,11 @@ export interface ITestExplorerFilterState { readonly excludeTags: ReadonlySet<string>; /** + * Whether fuzzy searching is enabled. + */ + readonly fuzzy: MutableObservableValue<boolean>; + + /** * Focuses the filter input in the test explorer view. */ focusInput(): void; @@ -81,10 +88,19 @@ export class TestExplorerFilterState implements ITestExplorerFilterState { /** @inheritdoc */ public readonly text = new MutableObservableValue(''); + /** @inheritdoc */ + public readonly fuzzy = MutableObservableValue.stored(new StoredValue<boolean>({ + key: 'testHistoryFuzzy', + scope: StorageScope.GLOBAL, + target: StorageTarget.USER, + }, this.storageService), false); + public readonly reveal = new MutableObservableValue</* test ID */string | undefined>(undefined); public readonly onDidRequestInputFocus = this.focusEmitter.event; + constructor(@IStorageService private readonly storageService: IStorageService) { } + /** @inheritdoc */ public focusInput() { this.focusEmitter.fire(); diff --git a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/common/testItemCollection.ts index c3409f6ba9e..06ad3b1b4b7 100644 --- a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testItemCollection.ts @@ -4,31 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { Barrier, isThenable, RunOnceScheduler } from 'vs/base/common/async'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { assertNever } from 'vs/base/common/types'; -// eslint-disable-next-line code-import-patterns -import { diffTestItems, ExtHostTestItemEvent, ExtHostTestItemEventOp, getPrivateApiFor, TestItemImpl, TestItemRootImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; -// eslint-disable-next-line code-import-patterns -import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { applyTestItemUpdate, ITestTag, namespaceTestTag, TestDiffOpType, TestItemExpandState, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testCollection'; +import { applyTestItemUpdate, ITestItem, ITestTag, namespaceTestTag, TestDiffOpType, TestItemExpandState, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; -import * as editorRange from 'vs/editor/common/core/range'; - -type TestItemRaw = Convert.TestItem.Raw; - -export interface IHierarchyProvider { - getChildren(node: TestItemRaw, token: CancellationToken): Iterable<TestItemRaw> | AsyncIterable<TestItemRaw> | undefined | null; -} /** * @private */ -export interface OwnedCollectionTestItem { +interface CollectionItem<T> { readonly fullId: TestId; readonly parent: TestId | null; - actual: TestItemImpl; + actual: T; expand: TestItemExpandState; /** * Number of levels of items below this one that are expanded. May be infinite. @@ -37,23 +25,144 @@ export interface OwnedCollectionTestItem { resolveBarrier?: Barrier; } +export const enum TestItemEventOp { + Upsert, + SetTags, + UpdateCanResolveChildren, + RemoveChild, + SetProp, + Bulk, +} + +export interface ITestItemUpsertChild { + op: TestItemEventOp.Upsert; + item: ITestItemLike; +} + +export interface ITestItemUpdateCanResolveChildren { + op: TestItemEventOp.UpdateCanResolveChildren; + state: boolean; +} + +export interface ITestItemSetTags { + op: TestItemEventOp.SetTags; + new: ITestTag[]; + old: ITestTag[]; +} + +export interface ITestItemRemoveChild { + op: TestItemEventOp.RemoveChild; + id: string; +} + +export interface ITestItemSetProp { + op: TestItemEventOp.SetProp; + update: Partial<ITestItem>; +} +export interface ITestItemBulkReplace { + op: TestItemEventOp.Bulk; + ops: (ITestItemUpsertChild | ITestItemRemoveChild)[]; +} + +export type ExtHostTestItemEvent = + | ITestItemSetTags + | ITestItemUpsertChild + | ITestItemRemoveChild + | ITestItemUpdateCanResolveChildren + | ITestItemSetProp + | ITestItemBulkReplace; + +export interface ITestItemApi<T> { + controllerId: string; + parent?: T; + listener?: (evt: ExtHostTestItemEvent) => void; +} + +export interface ITestItemCollectionOptions<T> { + /** Controller ID to use to prefix these test items. */ + controllerId: string; + + /** Gets API for the given test item, used to listen for events and set parents. */ + getApiFor(item: T): ITestItemApi<T>; + + /** Converts the full test item to the common interface. */ + toITestItem(item: T): ITestItem; + + /** Gets children for the item. */ + getChildren(item: T): ITestChildrenLike<T>; + + /** Root to use for the new test collection. */ + root: T; +} + +const strictEqualComparator = <T>(a: T, b: T) => a === b; +const diffableProps: { [K in keyof ITestItem]?: (a: ITestItem[K], b: ITestItem[K]) => boolean } = { + range: (a, b) => { + if (a === b) { return true; } + if (!a || !b) { return false; } + return a.equalsRange(b); + }, + busy: strictEqualComparator, + label: strictEqualComparator, + description: strictEqualComparator, + error: strictEqualComparator, + tags: (a, b) => { + if (a.length !== b.length) { + return false; + } + + if (a.some(t1 => !b.includes(t1))) { + return false; + } + + return true; + }, +}; + +const diffTestItems = (a: ITestItem, b: ITestItem) => { + let output: Record<string, unknown> | undefined; + for (const [key, cmp] of Object.entries(diffableProps) as [keyof ITestItem, (a: any, b: any) => boolean][]) { + if (!cmp(a[key], b[key])) { + if (output) { + output[key] = b[key]; + } else { + output = { [key]: b[key] }; + } + } + } + + return output as Partial<ITestItem> | undefined; +}; + +export interface ITestChildrenLike<T> extends Iterable<T> { + get(id: string): T | undefined; + delete(id: string): void; +} + +export interface ITestItemLike { + id: string; + tags: readonly ITestTag[]; + canResolveChildren: boolean; +} + /** - * Maintains tests created and registered for a single set of hierarchies - * for a workspace or document. - * @private + * Maintains a collection of test items for a single controller. */ -export class SingleUseTestCollection extends Disposable { +export class TestItemCollection<T extends ITestItemLike> extends Disposable { private readonly debounceSendDiff = this._register(new RunOnceScheduler(() => this.flushDiff(), 200)); private readonly diffOpEmitter = this._register(new Emitter<TestsDiff>()); - private _resolveHandler?: (item: TestItemRaw | undefined) => Promise<void> | void; + private _resolveHandler?: (item: T | undefined) => Promise<void> | void; + + public get root() { + return this.options.root; + } - public readonly root = new TestItemRootImpl(this.controllerId, this.controllerId); - public readonly tree = new Map</* full test id */string, OwnedCollectionTestItem>(); + public readonly tree = new Map</* full test id */string, CollectionItem<T>>(); private readonly tags = new Map<string, { label?: string; refCount: number }>(); protected diff: TestsDiff = []; - constructor(private readonly controllerId: string) { + constructor(private readonly options: ITestItemCollectionOptions<T>) { super(); this.root.canResolveChildren = true; this.upsertItem(this.root, undefined); @@ -62,7 +171,7 @@ export class SingleUseTestCollection extends Disposable { /** * Handler used for expanding test items. */ - public set resolveHandler(handler: undefined | ((item: TestItemRaw | undefined) => void)) { + public set resolveHandler(handler: undefined | ((item: T | undefined) => void)) { this._resolveHandler = handler; for (const test of this.tree.values()) { this.updateExpandability(test); @@ -139,7 +248,7 @@ export class SingleUseTestCollection extends Disposable { public override dispose() { for (const item of this.tree.values()) { - getPrivateApiFor(item.actual).listener = undefined; + this.options.getApiFor(item.actual).listener = undefined; } this.tree.clear(); @@ -147,68 +256,49 @@ export class SingleUseTestCollection extends Disposable { super.dispose(); } - private onTestItemEvent(internal: OwnedCollectionTestItem, evt: ExtHostTestItemEvent) { + private onTestItemEvent(internal: CollectionItem<T>, evt: ExtHostTestItemEvent) { switch (evt.op) { - case ExtHostTestItemEventOp.Invalidated: - this.pushDiff({ op: TestDiffOpType.Retire, itemId: internal.fullId.toString() }); - break; - - case ExtHostTestItemEventOp.RemoveChild: + case TestItemEventOp.RemoveChild: this.removeItem(TestId.joinToString(internal.fullId, evt.id)); break; - case ExtHostTestItemEventOp.Upsert: - this.upsertItem(evt.item, internal); + case TestItemEventOp.Upsert: + this.upsertItem(evt.item as T, internal); break; - case ExtHostTestItemEventOp.Bulk: + case TestItemEventOp.Bulk: for (const op of evt.ops) { this.onTestItemEvent(internal, op); } break; - case ExtHostTestItemEventOp.SetProp: { - const { key, value, previous } = evt; - const extId = internal.fullId.toString(); - switch (key) { - case 'canResolveChildren': - this.updateExpandability(internal); - break; - case 'tags': - this.diffTagRefs(value, previous, extId); - break; - case 'range': - this.pushDiff({ - op: TestDiffOpType.Update, - item: { extId, item: { range: editorRange.Range.lift(Convert.Range.from(value)) } }, - }); - break; - case 'error': - this.pushDiff({ op: TestDiffOpType.Update, item: { extId, item: { error: Convert.MarkdownString.fromStrict(value) || null }, } }); - break; - default: - this.pushDiff({ op: TestDiffOpType.Update, item: { extId, item: { [key]: value ?? null } } }); - break; - } + case TestItemEventOp.SetTags: + this.diffTagRefs(evt.new, evt.old, internal.fullId.toString()); + break; + + case TestItemEventOp.UpdateCanResolveChildren: + this.updateExpandability(internal); + break; + + case TestItemEventOp.SetProp: + this.pushDiff({ + op: TestDiffOpType.Update, + item: { extId: internal.fullId.toString(), item: evt.update } + }); break; - } default: assertNever(evt); } } - private upsertItem(actual: TestItemRaw, parent: OwnedCollectionTestItem | undefined) { - if (!(actual instanceof TestItemImpl)) { - throw new Error(`TestItems provided to the VS Code API must extend \`vscode.TestItem\`, but ${actual.id} did not`); - } - + private upsertItem(actual: T, parent: CollectionItem<T> | undefined) { const fullId = TestId.fromExtHostTestItem(actual, this.root.id, parent?.actual); // If this test item exists elsewhere in the tree already (exists at an // old ID with an existing parent), remove that old item. - const privateApi = getPrivateApiFor(actual); + const privateApi = this.options.getApiFor(actual); if (privateApi.parent && privateApi.parent !== parent?.actual) { - privateApi.parent.children.delete(actual.id); + this.options.getChildren(privateApi.parent).delete(actual.id); } let internal = this.tree.get(fullId.toString()); @@ -229,9 +319,9 @@ export class SingleUseTestCollection extends Disposable { op: TestDiffOpType.Add, item: { parent: internal.parent && internal.parent.toString(), - controllerId: this.controllerId, + controllerId: this.options.controllerId, expand: internal.expand, - item: Convert.TestItem.from(actual), + item: this.options.toITestItem(actual), }, }); @@ -246,28 +336,34 @@ export class SingleUseTestCollection extends Disposable { } // Case 3: upsert of an existing item by ID, with a new instance - const oldChildren = internal.actual.children; + const oldChildren = this.options.getChildren(internal.actual); const oldActual = internal.actual; - const changedProps = diffTestItems(oldActual, actual); - getPrivateApiFor(oldActual).listener = undefined; + const update = diffTestItems(this.options.toITestItem(oldActual), this.options.toITestItem(actual)); + this.options.getApiFor(oldActual).listener = undefined; internal.actual = actual; internal.expand = TestItemExpandState.NotExpandable; // updated by `connectItemAndChildren` - for (const [key, value] of changedProps) { - this.onTestItemEvent(internal, { op: ExtHostTestItemEventOp.SetProp, key, value, previous: oldActual[key] }); + + if (update) { + // tags are handled in a special way + if (update.hasOwnProperty('tags')) { + this.diffTagRefs(actual.tags, oldActual.tags, fullId.toString()); + delete update.tags; + } + this.onTestItemEvent(internal, { op: TestItemEventOp.SetProp, update }); } this.connectItemAndChildren(actual, internal, parent); // Remove any orphaned children. for (const child of oldChildren) { - if (!actual.children.get(child.id)) { + if (!this.options.getChildren(actual).get(child.id)) { this.removeItem(TestId.joinToString(fullId, child.id)); } } } - private diffTagRefs(newTags: ITestTag[], oldTags: ITestTag[], extId: string) { + private diffTagRefs(newTags: readonly ITestTag[], oldTags: readonly ITestTag[], extId: string) { const toDelete = new Set(oldTags.map(t => t.id)); for (const tag of newTags) { if (!toDelete.delete(tag.id)) { @@ -277,7 +373,7 @@ export class SingleUseTestCollection extends Disposable { this.pushDiff({ op: TestDiffOpType.Update, - item: { extId, item: { tags: newTags.map(v => Convert.TestTag.namespace(this.controllerId, v.id)) } } + item: { extId, item: { tags: newTags.map(v => namespaceTestTag(this.options.controllerId, v.id)) } } }); toDelete.forEach(this.decrementTagRefs, this); @@ -291,8 +387,7 @@ export class SingleUseTestCollection extends Disposable { this.tags.set(tag.id, { refCount: 1 }); this.pushDiff({ op: TestDiffOpType.AddTag, tag: { - id: Convert.TestTag.namespace(this.controllerId, tag.id), - ctrlLabel: this.root.label, + id: namespaceTestTag(this.options.controllerId, tag.id), } }); } @@ -302,27 +397,27 @@ export class SingleUseTestCollection extends Disposable { const existing = this.tags.get(tagId); if (existing && !--existing.refCount) { this.tags.delete(tagId); - this.pushDiff({ op: TestDiffOpType.RemoveTag, id: namespaceTestTag(this.controllerId, tagId) }); + this.pushDiff({ op: TestDiffOpType.RemoveTag, id: namespaceTestTag(this.options.controllerId, tagId) }); } } - private setItemParent(actual: TestItemImpl, parent: OwnedCollectionTestItem | undefined) { - getPrivateApiFor(actual).parent = parent && parent.actual !== this.root ? parent.actual : undefined; + private setItemParent(actual: T, parent: CollectionItem<T> | undefined) { + this.options.getApiFor(actual).parent = parent && parent.actual !== this.root ? parent.actual : undefined; } - private connectItem(actual: TestItemImpl, internal: OwnedCollectionTestItem, parent: OwnedCollectionTestItem | undefined) { + private connectItem(actual: T, internal: CollectionItem<T>, parent: CollectionItem<T> | undefined) { this.setItemParent(actual, parent); - const api = getPrivateApiFor(actual); + const api = this.options.getApiFor(actual); api.parent = parent?.actual; api.listener = evt => this.onTestItemEvent(internal, evt); this.updateExpandability(internal); } - private connectItemAndChildren(actual: TestItemImpl, internal: OwnedCollectionTestItem, parent: OwnedCollectionTestItem | undefined) { + private connectItemAndChildren(actual: T, internal: CollectionItem<T>, parent: CollectionItem<T> | undefined) { this.connectItem(actual, internal, parent); // Discover any existing children that might have already been added - for (const child of actual.children) { + for (const child of this.options.getChildren(actual)) { this.upsertItem(child, internal); } } @@ -332,7 +427,7 @@ export class SingleUseTestCollection extends Disposable { * resolved state of the item changes. Can automatically expand the item * if requested by a consumer. */ - private updateExpandability(internal: OwnedCollectionTestItem) { + private updateExpandability(internal: CollectionItem<T>) { let newState: TestItemExpandState; if (!this._resolveHandler) { newState = TestItemExpandState.NotExpandable; @@ -363,13 +458,13 @@ export class SingleUseTestCollection extends Disposable { * the children will be expanded. If it's 1, the children and their children * will be expanded. If it's <0, it's a no-op. */ - private expandChildren(internal: OwnedCollectionTestItem, levels: number): Promise<void> | void { + private expandChildren(internal: CollectionItem<T>, levels: number): Promise<void> | void { if (levels < 0) { return; } const expandRequests: Promise<void>[] = []; - for (const child of internal.actual.children) { + for (const child of this.options.getChildren(internal.actual)) { const promise = this.expand(TestId.joinToString(internal.fullId, child.id), levels); if (isThenable(promise)) { expandRequests.push(promise); @@ -384,7 +479,7 @@ export class SingleUseTestCollection extends Disposable { /** * Calls `discoverChildren` on the item, refreshing all its tests. */ - private resolveChildren(internal: OwnedCollectionTestItem) { + private resolveChildren(internal: CollectionItem<T>) { if (internal.resolveBarrier) { return internal.resolveBarrier; } @@ -400,10 +495,7 @@ export class SingleUseTestCollection extends Disposable { const barrier = internal.resolveBarrier = new Barrier(); const applyError = (err: Error) => { - console.error(`Unhandled error in resolveHandler of test controller "${this.controllerId}"`); - if (internal.actual !== this.root) { - internal.actual.error = err.stack || err.message || String(err); - } + console.error(`Unhandled error in resolveHandler of test controller "${this.options.controllerId}"`, err); }; let r: Thenable<void> | void; @@ -426,7 +518,7 @@ export class SingleUseTestCollection extends Disposable { return internal.resolveBarrier; } - private pushExpandStateUpdate(internal: OwnedCollectionTestItem) { + private pushExpandStateUpdate(internal: CollectionItem<T>) { this.pushDiff({ op: TestDiffOpType.Update, item: { extId: internal.fullId.toString(), expand: internal.expand } }); } @@ -438,21 +530,21 @@ export class SingleUseTestCollection extends Disposable { this.pushDiff({ op: TestDiffOpType.Remove, itemId: childId }); - const queue: (OwnedCollectionTestItem | undefined)[] = [childItem]; + const queue: (CollectionItem<T> | undefined)[] = [childItem]; while (queue.length) { const item = queue.pop(); if (!item) { continue; } - getPrivateApiFor(item.actual).listener = undefined; + this.options.getApiFor(item.actual).listener = undefined; for (const tag of item.actual.tags) { this.decrementTagRefs(tag.id); } this.tree.delete(item.fullId.toString()); - for (const child of item.actual.children) { + for (const child of this.options.getChildren(item.actual)) { queue.push(this.tree.get(TestId.joinToString(item.fullId, child.id))); } } @@ -468,3 +560,120 @@ export class SingleUseTestCollection extends Disposable { } } } + +/** Implementation os vscode.TestItemCollection */ +export interface ITestItemChildren<T extends ITestItemLike> extends Iterable<T> { + readonly size: number; + replace(items: readonly T[]): void; + forEach(callback: (item: T, collection: this) => unknown, thisArg?: unknown): void; + add(item: T): void; + delete(itemId: string): void; + get(itemId: string): T | undefined; + + toJSON(): readonly T[]; +} + +export class DuplicateTestItemError extends Error { + constructor(id: string) { + super(`Attempted to insert a duplicate test item ID ${id}`); + } +} + +export class InvalidTestItemError extends Error { + constructor(id: string) { + super(`TestItem with ID "${id}" is invalid. Make sure to create it from the createTestItem method.`); + } +} + +export class MixedTestItemController extends Error { + constructor(id: string, ctrlA: string, ctrlB: string) { + super(`TestItem with ID "${id}" is from controller "${ctrlA}" and cannot be added as a child of an item from controller "${ctrlB}".`); + } +} + +export const createTestItemChildren = <T extends ITestItemLike>(api: ITestItemApi<T>, getApi: (item: T) => ITestItemApi<T>, checkCtor: Function): ITestItemChildren<T> => { + let mapped = new Map<string, T>(); + + return { + /** @inheritdoc */ + get size() { + return mapped.size; + }, + + /** @inheritdoc */ + forEach(callback: (item: T, collection: ITestItemChildren<T>) => unknown, thisArg?: unknown) { + for (const item of mapped.values()) { + callback.call(thisArg, item, this); + } + }, + + /** @inheritdoc */ + replace(items: Iterable<T>) { + const newMapped = new Map<string, T>(); + const toDelete = new Set(mapped.keys()); + const bulk: ITestItemBulkReplace = { op: TestItemEventOp.Bulk, ops: [] }; + + for (const item of items) { + if (!(item instanceof checkCtor)) { + throw new InvalidTestItemError(item.id); + } + + const itemController = getApi(item).controllerId; + if (itemController !== api.controllerId) { + throw new MixedTestItemController(item.id, itemController, api.controllerId); + } + + if (newMapped.has(item.id)) { + throw new DuplicateTestItemError(item.id); + } + + newMapped.set(item.id, item); + toDelete.delete(item.id); + bulk.ops.push({ op: TestItemEventOp.Upsert, item }); + } + + for (const id of toDelete.keys()) { + bulk.ops.push({ op: TestItemEventOp.RemoveChild, id }); + } + + api.listener?.(bulk); + + // important mutations come after firing, so if an error happens no + // changes will be "saved": + mapped = newMapped; + }, + + + /** @inheritdoc */ + add(item: T) { + if (!(item instanceof checkCtor)) { + throw new InvalidTestItemError(item.id); + } + + mapped.set(item.id, item); + api.listener?.({ op: TestItemEventOp.Upsert, item }); + }, + + /** @inheritdoc */ + delete(id: string) { + if (mapped.delete(id)) { + api.listener?.({ op: TestItemEventOp.RemoveChild, id }); + } + }, + + /** @inheritdoc */ + get(itemId: string) { + return mapped.get(itemId); + }, + + /** JSON serialization function. */ + toJSON() { + return Array.from(mapped.values()); + }, + + /** @inheritdoc */ + [Symbol.iterator]() { + return mapped.values(); + }, + }; +}; diff --git a/src/vs/workbench/contrib/testing/common/testProfileService.ts b/src/vs/workbench/contrib/testing/common/testProfileService.ts index 7c83d34d371..ecdb9ffcd63 100644 --- a/src/vs/workbench/contrib/testing/common/testProfileService.ts +++ b/src/vs/workbench/contrib/testing/common/testProfileService.ts @@ -9,7 +9,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { InternalTestItem, ITestRunProfile, TestRunProfileBitset, testRunProfileBitsetList } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, ITestRunProfile, TestRunProfileBitset, testRunProfileBitsetList } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { IMainThreadTestController } from 'vs/workbench/contrib/testing/common/testService'; diff --git a/src/vs/workbench/contrib/testing/common/testResult.ts b/src/vs/workbench/contrib/testing/common/testResult.ts index ad80e64dff4..dce023d7d60 100644 --- a/src/vs/workbench/contrib/testing/common/testResult.ts +++ b/src/vs/workbench/contrib/testing/common/testResult.ts @@ -12,7 +12,7 @@ import { Range } from 'vs/editor/common/core/range'; import { localize } from 'vs/nls'; import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState'; import { IObservableValue, MutableObservableValue, staticObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; -import { IRichLocation, ISerializedTestResults, ITestItem, ITestMessage, ITestOutputMessage, ITestRunTask, ITestTaskState, ResolvedTestRunRequest, TestItemExpandState, TestMessageType, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { IRichLocation, ISerializedTestResults, ITestItem, ITestMessage, ITestOutputMessage, ITestRunTask, ITestTaskState, ResolvedTestRunRequest, TestItemExpandState, TestMessageType, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { maxPriority, statesInOrder, terminalStatePriorities } from 'vs/workbench/contrib/testing/common/testingStates'; @@ -230,18 +230,15 @@ const itemToNode = (controllerId: string, item: ITestItem, parent: string | null tasks: [], ownComputedState: TestResultState.Unset, computedState: TestResultState.Unset, - retired: false, }); export const enum TestResultItemChangeReason { - Retired, - ParentRetired, ComputedStateChange, OwnStateChange, } export type TestResultItemChange = { item: TestResultItem; result: ITestResult } & ( - | { reason: TestResultItemChangeReason.Retired | TestResultItemChangeReason.ParentRetired | TestResultItemChangeReason.ComputedStateChange } + | { reason: TestResultItemChangeReason.ComputedStateChange } | { reason: TestResultItemChangeReason.OwnStateChange; previousState: TestResultState; previousOwnDuration: number | undefined } ); @@ -420,33 +417,6 @@ export class LiveTestResult implements ITestResult { } /** - * Marks a test as retired. This can trigger it to be rerun in live mode. - */ - public retire(testId: string) { - const root = this.testById.get(testId); - if (!root || root.retired) { - return; - } - - const queue = [[root]]; - while (queue.length) { - for (const entry of queue.pop()!) { - if (!entry.retired) { - entry.retired = true; - queue.push(entry.children); - this.changeEmitter.fire({ - result: this, - item: entry, - reason: entry === root - ? TestResultItemChangeReason.Retired - : TestResultItemChangeReason.ParentRetired - }); - } - } - } - } - - /** * Marks the task in the test run complete. */ public markTaskComplete(taskId: string) { @@ -638,7 +608,7 @@ export class HydratedTestResult implements ITestResult { this.request = serialized.request; for (const item of serialized.items) { - const cast: TestResultItem = { ...item, retired: true } as any; + const cast: TestResultItem = { ...item } as any; cast.item.uri = URI.revive(cast.item.uri); for (const task of cast.tasks) { diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index 061f3694be8..5c3211c0ad1 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -11,7 +11,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { generateUuid } from 'vs/base/common/uuid'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ExtensionRunTestsRequest, ITestRunProfile, ResolvedTestRunRequest, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ExtensionRunTestsRequest, ITestRunProfile, ResolvedTestRunRequest, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { ITestResult, LiveTestResult, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; diff --git a/src/vs/workbench/contrib/testing/common/testResultStorage.ts b/src/vs/workbench/contrib/testing/common/testResultStorage.ts index 6c272186949..d2d294aa081 100644 --- a/src/vs/workbench/contrib/testing/common/testResultStorage.ts +++ b/src/vs/workbench/contrib/testing/common/testResultStorage.ts @@ -14,7 +14,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { ISerializedTestResults } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ISerializedTestResults } from 'vs/workbench/contrib/testing/common/testTypes'; import { HydratedTestResult, ITestResult, LiveOutputController, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; export const RETAIN_MAX_RESULTS = 128; diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index 68509583b60..7c2450a2e94 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -12,7 +12,7 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IObservableValue, MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; -import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, ITestItemContext, ResolvedTestRunRequest, RunTestForControllerRequest, TestItemExpandState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, ITestItemContext, ResolvedTestRunRequest, RunTestForControllerRequest, TestItemExpandState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestExclusions } from 'vs/workbench/contrib/testing/common/testExclusions'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 083d40f7801..efc2afbd0ca 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -17,7 +17,7 @@ import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/work import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/mainThreadTestCollection'; import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { ResolvedTestRunRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ResolvedTestRunRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestExclusions } from 'vs/workbench/contrib/testing/common/testExclusions'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index 4a50a4c7d9e..6f3e639ef90 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -229,7 +229,7 @@ export interface ITestRunTask { } export interface ITestTag { - id: string; + readonly id: string; } const testTagDelimiter = '\0'; @@ -244,7 +244,6 @@ export const denamespaceTestTag = (namespaced: string) => { export interface ITestTagDisplayInfo { id: string; - ctrlLabel: string; } /** @@ -419,8 +418,6 @@ export interface TestResultItem extends InternalTestItem { ownComputedState: TestResultState; /** Computed state based on children */ computedState: TestResultState; - /** True if the test is outdated */ - retired: boolean; /** Max duration of the item's tasks (if run directly) */ ownDuration?: number; } @@ -564,7 +561,7 @@ export interface ITestItemContext { /** Marshalling marker */ $mid: MarshalledId.TestItemContext; /** Tests and parents from the root to the current items */ - tests: InternalTestItem[]; + tests: InternalTestItem.Serialized[]; } /** diff --git a/src/vs/workbench/contrib/testing/common/testingAutoRun.ts b/src/vs/workbench/contrib/testing/common/testingAutoRun.ts deleted file mode 100644 index 905b7370f17..00000000000 --- a/src/vs/workbench/contrib/testing/common/testingAutoRun.ts +++ /dev/null @@ -1,148 +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 { RunOnceScheduler } from 'vs/base/common/async'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Iterable } from 'vs/base/common/iterator'; -import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { AutoRunMode, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; -import { InternalTestItem, TestDiffOpType, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; -import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; -import { TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; -import { isRunningTests, ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { getCollectionItemParents, ITestService } from 'vs/workbench/contrib/testing/common/testService'; - -export interface ITestingAutoRun { - /** - * Toggles autorun on or off. - */ - toggle(): void; -} - -export const ITestingAutoRun = createDecorator<ITestingAutoRun>('testingAutoRun'); - -export class TestingAutoRun extends Disposable implements ITestingAutoRun { - private enabled: IContextKey<boolean>; - private runner = this._register(new MutableDisposable()); - - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @ITestService private readonly testService: ITestService, - @ITestResultService private readonly results: ITestResultService, - @IConfigurationService private readonly configuration: IConfigurationService, - ) { - super(); - this.enabled = TestingContextKeys.autoRun.bindTo(contextKeyService); - - this._register(configuration.onDidChangeConfiguration(evt => { - if (evt.affectsConfiguration(TestingConfigKeys.AutoRunMode) && this.enabled.get()) { - this.runner.value = this.makeRunner(); - } - })); - } - - /** - * @inheritdoc - */ - public toggle(): void { - const enabled = this.enabled.get(); - if (enabled) { - this.runner.value = undefined; - } else { - this.runner.value = this.makeRunner(); - } - - this.enabled.set(!enabled); - } - - /** - * Creates the runner. Is triggered when tests are marked as retired. - * Runs them on a debounce. - */ - private makeRunner() { - const rerunIds = new Map<string, InternalTestItem>(); - const store = new DisposableStore(); - const cts = new CancellationTokenSource(); - store.add(toDisposable(() => cts.dispose(true))); - - let delay = getTestingConfiguration(this.configuration, TestingConfigKeys.AutoRunDelay); - - store.add(this.configuration.onDidChangeConfiguration(() => { - delay = getTestingConfiguration(this.configuration, TestingConfigKeys.AutoRunDelay); - })); - - const scheduler = store.add(new RunOnceScheduler(async () => { - if (rerunIds.size === 0) { - return; - } - - const tests = [...rerunIds.values()]; - rerunIds.clear(); - await this.testService.runTests({ group: TestRunProfileBitset.Run, tests, isAutoRun: true }); - - if (rerunIds.size > 0) { - scheduler.schedule(delay); - } - }, delay)); - - const addToRerun = (test: InternalTestItem) => { - rerunIds.set(test.item.extId, test); - if (!isRunningTests(this.results)) { - scheduler.schedule(delay); - } - }; - - const removeFromRerun = (test: InternalTestItem) => { - rerunIds.delete(test.item.extId); - if (rerunIds.size === 0) { - scheduler.cancel(); - } - }; - - store.add(this.results.onTestChanged(evt => { - if (evt.reason === TestResultItemChangeReason.Retired) { - addToRerun(evt.item); - } else if ((evt.reason === TestResultItemChangeReason.OwnStateChange || evt.reason === TestResultItemChangeReason.ComputedStateChange)) { - removeFromRerun(evt.item); - } - })); - - store.add(this.results.onResultsChanged(evt => { - if ('completed' in evt && !isRunningTests(this.results) && rerunIds.size) { - scheduler.schedule(0); - } - })); - - if (getTestingConfiguration(this.configuration, TestingConfigKeys.AutoRunMode) === AutoRunMode.AllInWorkspace) { - - store.add(this.testService.onDidProcessDiff(diff => { - for (const entry of diff) { - if (entry.op === TestDiffOpType.Add) { - const test = entry.item; - const isQueued = Iterable.some( - getCollectionItemParents(this.testService.collection, test), - t => rerunIds.has(test.item.extId), - ); - - const state = this.results.getStateById(test.item.extId); - if (!isQueued && (!state || state[1].retired)) { - addToRerun(test); - } - } - } - })); - - - for (const root of this.testService.collection.rootItems) { - addToRerun(root); - } - } - - return store; - } -} diff --git a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts index 2f64387a111..c93c0968a07 100644 --- a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts +++ b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts @@ -9,7 +9,7 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { TestMessageType } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestMessageType } from 'vs/workbench/contrib/testing/common/testTypes'; import { parseTestUri, TestUriType, TEST_DATA_SCHEME } from 'vs/workbench/contrib/testing/common/testingUri'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index d43cb0e7af1..2663b018541 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { TestExplorerViewMode, TestExplorerViewSorting } from 'vs/workbench/contrib/testing/common/constants'; -import { TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; export namespace TestingContextKeys { export const providerCount = new RawContextKey('testing.providerCount', 0); diff --git a/src/vs/workbench/contrib/testing/common/testingDecorations.ts b/src/vs/workbench/contrib/testing/common/testingDecorations.ts index 98abb6b86ec..3a148eb3e97 100644 --- a/src/vs/workbench/contrib/testing/common/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/common/testingDecorations.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestMessage } from 'vs/workbench/contrib/testing/common/testTypes'; export interface ITestingDecorationsService { _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts b/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts index b3fcba06951..4d999302c9e 100644 --- a/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts +++ b/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts @@ -6,7 +6,7 @@ import { URI } from 'vs/base/common/uri'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestResultItem } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; export interface ITestingPeekOpener { diff --git a/src/vs/workbench/contrib/testing/common/testingStates.ts b/src/vs/workbench/contrib/testing/common/testingStates.ts index cb322d13399..38ea70ce4bf 100644 --- a/src/vs/workbench/contrib/testing/common/testingStates.ts +++ b/src/vs/workbench/contrib/testing/common/testingStates.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; export type TreeStateNode = { statusNode: true; state: TestResultState; priority: number }; @@ -16,8 +16,8 @@ export const statePriority: { [K in TestResultState]: number } = { [TestResultState.Running]: 6, [TestResultState.Errored]: 5, [TestResultState.Failed]: 4, - [TestResultState.Passed]: 3, - [TestResultState.Queued]: 2, + [TestResultState.Queued]: 3, + [TestResultState.Passed]: 2, [TestResultState.Unset]: 1, [TestResultState.Skipped]: 0, }; diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts index 66bf4f2db54..af584d316c4 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts @@ -7,11 +7,11 @@ import * as assert from 'assert'; import { AbstractTreeViewState } from 'vs/base/browser/ui/tree/abstractTree'; import { Emitter } from 'vs/base/common/event'; import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; -import { TestDiffOpType, TestItemExpandState, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestDiffOpType, TestItemExpandState, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; -import { Convert, TestItemImpl } from 'vs/workbench/contrib/testing/test/common/testStubs'; import { TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; +import { TestTestItem } from 'vs/workbench/contrib/testing/test/common/testStubs'; class TestHierarchicalByLocationProjection extends HierarchicalByLocationProjection { } @@ -55,10 +55,10 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { harness.flush(); harness.pushDiff({ op: TestDiffOpType.Add, - item: { controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: Convert.TestItem.from(new TestItemImpl('ctrl2', 'c', 'c', undefined)) }, + item: { controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: new TestTestItem('ctrl2', 'c', 'c').toTestItem() }, }, { op: TestDiffOpType.Add, - item: { controllerId: 'ctrl2', parent: new TestId(['ctrl2', 'c']).toString(), expand: TestItemExpandState.NotExpandable, item: Convert.TestItem.from(new TestItemImpl('ctrl2', 'c-a', 'ca', undefined)) }, + item: { controllerId: 'ctrl2', parent: new TestId(['ctrl2', 'c']).toString(), expand: TestItemExpandState.NotExpandable, item: new TestTestItem('ctrl2', 'c-a', 'ca').toTestItem() }, }); assert.deepStrictEqual(harness.flush(), [ @@ -76,7 +76,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { { e: 'b' } ]); - harness.c.root.children.get('id-a')!.children.add(new TestItemImpl('ctrlId', 'ac', 'ac', undefined)); + harness.c.root.children.get('id-a')!.children.add(new TestTestItem('ctrlId', 'ac', 'ac')); assert.deepStrictEqual(harness.flush(), [ { e: 'a', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'ac' }] }, @@ -119,7 +119,6 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { }, parent: 'id-root', tasks: [], - retired: false, ownComputedState: state, computedState: state, expand: 0, diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts index 8e5b439a920..ec4424b8edd 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts @@ -7,11 +7,11 @@ import * as assert from 'assert'; import { AbstractTreeViewState } from 'vs/base/browser/ui/tree/abstractTree'; import { Emitter } from 'vs/base/common/event'; import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; -import { TestDiffOpType, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestDiffOpType, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestResultItemChange } from 'vs/workbench/contrib/testing/common/testResult'; -import { Convert, TestItemImpl } from 'vs/workbench/contrib/testing/test/common/testStubs'; import { TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; +import { TestTestItem } from 'vs/workbench/contrib/testing/test/common/testStubs'; suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { let harness: TestTreeTestHarness<HierarchicalByNameProjection>; @@ -44,10 +44,10 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { harness.flush(); harness.pushDiff({ op: TestDiffOpType.Add, - item: { controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: Convert.TestItem.from(new TestItemImpl('ctrl2', 'c', 'root2', undefined)) }, + item: { controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: new TestTestItem('ctrl2', 'c', 'root2').toTestItem() }, }, { op: TestDiffOpType.Add, - item: { controllerId: 'ctrl2', parent: new TestId(['ctrl2', 'c']).toString(), expand: TestItemExpandState.NotExpandable, item: Convert.TestItem.from(new TestItemImpl('ctrl2', 'c-a', 'c', undefined)) }, + item: { controllerId: 'ctrl2', parent: new TestId(['ctrl2', 'c']).toString(), expand: TestItemExpandState.NotExpandable, item: new TestTestItem('ctrl2', 'c-a', 'c', undefined).toTestItem() }, }); assert.deepStrictEqual(harness.flush(), [ @@ -59,7 +59,7 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { test('updates nodes if they add children', async () => { harness.flush(); - harness.c.root.children.get('id-a')!.children.add(new TestItemImpl('ctrl2', 'ac', 'ac', undefined)); + harness.c.root.children.get('id-a')!.children.add(new TestTestItem('ctrl2', 'ac', 'ac')); assert.deepStrictEqual(harness.flush(), [ { e: 'aa' }, @@ -81,7 +81,7 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { test('swaps when node is no longer leaf', async () => { harness.flush(); - harness.c.root.children.get('id-b')!.children.add(new TestItemImpl('ctrl2', 'ba', 'ba', undefined)); + harness.c.root.children.get('id-b')!.children.add(new TestTestItem('ctrl2', 'ba', 'ba')); assert.deepStrictEqual(harness.flush(), [ { e: 'aa' }, diff --git a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts index d5dd271b36e..9fd9b315d93 100644 --- a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts +++ b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts @@ -9,7 +9,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/mainThreadTestCollection'; -import { TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { testStubs } from 'vs/workbench/contrib/testing/test/common/testStubs'; diff --git a/src/vs/workbench/contrib/testing/test/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/test/common/ownedTestCollection.ts deleted file mode 100644 index ce29ffa8119..00000000000 --- a/src/vs/workbench/contrib/testing/test/common/ownedTestCollection.ts +++ /dev/null @@ -1,17 +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 { SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; -import { TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; - -export class TestSingleUseCollection extends SingleUseTestCollection { - public get currentDiff() { - return this.diff; - } - - public setDiff(diff: TestsDiff) { - this.diff = diff; - } -} diff --git a/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts b/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts index 1b4657d5600..cc89323ebdd 100644 --- a/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { InMemoryStorageService } from 'vs/platform/storage/common/storage'; import { TestExplorerFilterState, TestFilterTerm } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; suite('TestExplorerFilterState', () => { let t: TestExplorerFilterState; setup(() => { - t = new TestExplorerFilterState(); + t = new TestExplorerFilterState(new InMemoryStorageService()); }); const assertFilteringFor = (expected: { [T in TestFilterTerm]?: boolean }) => { diff --git a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts index ea9d89f01ce..4452c491436 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts @@ -9,14 +9,13 @@ import { bufferToStream, newWriteableBufferStream, VSBuffer } from 'vs/base/comm import { Lazy } from 'vs/base/common/lazy'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { NullLogService } from 'vs/platform/log/common/log'; -import { SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; -import { ITestTaskState, ResolvedTestRunRequest, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestTaskState, ResolvedTestRunRequest, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { HydratedTestResult, LiveOutputController, LiveTestResult, makeEmptyCounts, resultItemParents, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { TestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { InMemoryResultStorage, ITestResultStorage } from 'vs/workbench/contrib/testing/common/testResultStorage'; -import { Convert, getInitializedMainTestCollection, TestItemImpl, testStubs } from 'vs/workbench/contrib/testing/test/common/testStubs'; +import { getInitializedMainTestCollection, testStubs, TestTestCollection } from 'vs/workbench/contrib/testing/test/common/testStubs'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; export const emptyOutputController = () => new LiveOutputController( @@ -32,7 +31,7 @@ suite('Workbench - Test Results Service', () => { let r: TestLiveTestResult; let changed = new Set<TestResultItemChange>(); - let tests: SingleUseTestCollection; + let tests: TestTestCollection; const defaultOpts = (testIds: string[]): ResolvedTestRunRequest => ({ targets: [{ @@ -72,16 +71,15 @@ suite('Workbench - Test Results Service', () => { throw new Error('timed out while expanding, diff: ' + JSON.stringify(tests.collectDiff())); } - r.addTestChainToRun('ctrlId', [ - Convert.TestItem.from(tests.root), - Convert.TestItem.from(tests.root.children.get('id-a') as TestItemImpl), - Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-aa') as TestItemImpl), + tests.root.toTestItem(), + tests.root.children.get('id-a')!.toTestItem(), + tests.root.children.get('id-a')!.children.get('id-aa')!.toTestItem(), ]); r.addTestChainToRun('ctrlId', [ - Convert.TestItem.from(tests.root.children.get('id-a') as TestItemImpl), - Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-ab') as TestItemImpl), + tests.root.children.get('id-a')!.toTestItem(), + tests.root.children.get('id-a')!.children.get('id-ab')!.toTestItem(), ]); }); @@ -171,20 +169,6 @@ suite('Workbench - Test Results Service', () => { assert.deepStrictEqual(r.getStateById(testId)?.ownComputedState, TestResultState.Errored); }); - test('retire', () => { - changed.clear(); - r.retire(new TestId(['ctrlId', 'id-a']).toString()); - assert.deepStrictEqual(getChangeSummary(), [ - { label: 'a', reason: TestResultItemChangeReason.Retired }, - { label: 'aa', reason: TestResultItemChangeReason.ParentRetired }, - { label: 'ab', reason: TestResultItemChangeReason.ParentRetired }, - ]); - - changed.clear(); - r.retire(new TestId(['ctrlId', 'id-a']).toString()); - assert.strictEqual(changed.size, 0); - }); - test('ignores outside run', () => { changed.clear(); r.updateState(new TestId(['ctrlId', 'id-b']).toString(), 't', TestResultState.Running); @@ -252,7 +236,7 @@ suite('Workbench - Test Results Service', () => { const expected: any = { ...r.getStateById(tests.root.id)! }; expected.item.uri = actual.item.uri; expected.item.children = actual.item.children; - assert.deepStrictEqual(actual, { ...expected, retired: true, children: [new TestId(['ctrlId', 'id-a']).toString()] }); + assert.deepStrictEqual(actual, { ...expected, children: [new TestId(['ctrlId', 'id-a']).toString()] }); assert.deepStrictEqual(rehydrated.counts, r.counts); assert.strictEqual(typeof rehydrated.completedAt, 'number'); }); diff --git a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts index 0fd486588e7..e10d994eb03 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts @@ -9,8 +9,8 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { InMemoryResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage'; -import { Convert, TestItemImpl, testStubs } from 'vs/workbench/contrib/testing/test/common/testStubs'; import { emptyOutputController } from 'vs/workbench/contrib/testing/test/common/testResultService.test'; +import { testStubs } from 'vs/workbench/contrib/testing/test/common/testStubs'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; suite('Workbench - Test Result Storage', () => { @@ -28,9 +28,9 @@ suite('Workbench - Test Result Storage', () => { const tests = testStubs.nested(); tests.expand(tests.root.id, Infinity); t.addTestChainToRun('ctrlId', [ - Convert.TestItem.from(tests.root), - Convert.TestItem.from(tests.root.children.get('id-a') as TestItemImpl), - Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-aa') as TestItemImpl), + tests.root.toTestItem(), + tests.root.children.get('id-a')!.toTestItem(), + tests.root.children.get('id-a')!.children.get('id-aa')!.toTestItem(), ]); if (addMessage) { diff --git a/src/vs/workbench/contrib/testing/test/common/testStubs.ts b/src/vs/workbench/contrib/testing/test/common/testStubs.ts index 9ef3d39af94..9c9bb140bc2 100644 --- a/src/vs/workbench/contrib/testing/test/common/testStubs.ts +++ b/src/vs/workbench/contrib/testing/test/common/testStubs.ts @@ -4,15 +4,95 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -// eslint-disable-next-line code-import-patterns -import { TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/mainThreadTestCollection'; -import { TestSingleUseCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; +import { ITestItem, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { createTestItemChildren, ITestItemApi, ITestItemLike, TestItemCollection, TestItemEventOp } from 'vs/workbench/contrib/testing/common/testItemCollection'; -// eslint-disable-next-line code-import-patterns -export * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; -// eslint-disable-next-line code-import-patterns -export { TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; +export class TestTestItem implements ITestItemLike { + private readonly props: ITestItem; + private _canResolveChildren = false; + + public get tags() { + return this.props.tags.map(id => ({ id })); + } + + public set tags(value) { + this.api.listener?.({ op: TestItemEventOp.SetTags, new: value, old: this.props.tags.map(t => ({ id: t })) }); + this.props.tags = value.map(tag => tag.id); + } + + public get canResolveChildren() { + return this._canResolveChildren; + } + + public set canResolveChildren(value: boolean) { + this._canResolveChildren = value; + this.api.listener?.({ op: TestItemEventOp.UpdateCanResolveChildren, state: value }); + } + + public get parent() { + return this.api.parent; + } + + public api: ITestItemApi<TestTestItem> = { controllerId: this.controllerId }; + + public children = createTestItemChildren(this.api, i => i.api, TestTestItem); + + constructor( + public readonly controllerId: string, + public readonly id: string, + label: string, + uri?: URI, + ) { + this.props = { + extId: '', + busy: false, + description: null, + error: null, + label, + range: null, + sortText: null, + tags: [], + uri, + }; + } + + public get<K extends keyof ITestItem>(key: K): ITestItem[K] { + return this.props[key]; + } + + public set<K extends keyof ITestItem>(key: K, value: ITestItem[K]) { + this.props[key] = value; + this.api.listener?.({ op: TestItemEventOp.SetProp, update: { [key]: value } }); + } + + public toTestItem(): ITestItem { + const props = { ...this.props }; + props.extId = TestId.fromExtHostTestItem(this, this.controllerId).toString(); + return props; + } +} + +export class TestTestCollection extends TestItemCollection<TestTestItem> { + constructor(controllerId = 'ctrlId') { + super({ + controllerId, + getApiFor: t => t.api, + toITestItem: t => t.toTestItem(), + getChildren: t => t.children, + root: new TestTestItem(controllerId, controllerId, 'root'), + }); + } + + public get currentDiff() { + return this.diff; + } + + public setDiff(diff: TestsDiff) { + this.diff = diff; + } +} /** * Gets a main thread test collection initialized with the given set of @@ -27,19 +107,17 @@ export const getInitializedMainTestCollection = async (singleUse = testStubs.nes export const testStubs = { nested: (idPrefix = 'id-') => { - const collection = new TestSingleUseCollection('ctrlId'); - collection.root.label = 'root'; + const collection = new TestTestCollection(); collection.resolveHandler = item => { if (item === undefined) { - const a = new TestItemImpl('ctrlId', idPrefix + 'a', 'a', URI.file('/')); + const a = new TestTestItem('ctrlId', idPrefix + 'a', 'a', URI.file('/')); a.canResolveChildren = true; - const b = new TestItemImpl('ctrlId', idPrefix + 'b', 'b', URI.file('/')); - collection.root.children.replace([a, b]); + const b = new TestTestItem('ctrlId', idPrefix + 'b', 'b', URI.file('/')); + collection.root.children.add(a); + collection.root.children.add(b); } else if (item.id === idPrefix + 'a') { - item.children.replace([ - new TestItemImpl('ctrlId', idPrefix + 'aa', 'aa', URI.file('/')), - new TestItemImpl('ctrlId', idPrefix + 'ab', 'ab', URI.file('/')), - ]); + item.children.add(new TestTestItem('ctrlId', idPrefix + 'aa', 'aa', URI.file('/'))); + item.children.add(new TestTestItem('ctrlId', idPrefix + 'ab', 'ab', URI.file('/'))); } }; diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index efb577a072b..1c312d73bdd 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -476,7 +476,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } if (this._webviewFindWidget) { - parent.appendChild(this._webviewFindWidget.getDomNode()!); + parent.appendChild(this._webviewFindWidget.getDomNode()); } parent.appendChild(this.element); } diff --git a/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts b/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts index fde2df743dd..e7bad42e252 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts @@ -20,6 +20,9 @@ export interface WebviewFindDelegate { } export class WebviewFindWidget extends SimpleFindWidget { + protected async _getResultCount(dataChanged?: boolean): Promise<{ resultIndex: number; resultCount: number } | undefined> { + return undefined; + } protected readonly _findWidgetFocused: IContextKey<boolean>; @@ -28,7 +31,7 @@ export class WebviewFindWidget extends SimpleFindWidget { @IContextViewService contextViewService: IContextViewService, @IContextKeyService contextKeyService: IContextKeyService ) { - super(contextViewService, contextKeyService, undefined, false, _delegate.checkImeCompletionState); + super(contextViewService, contextKeyService, undefined, { showOptionButtons: false, checkImeCompletionState: _delegate.checkImeCompletionState }); this._findWidgetFocused = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED.bindTo(contextKeyService); this._register(_delegate.hasFindResult(hasResult => { diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index f7beac7c5bb..e6a1076d917 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -6,7 +6,7 @@ import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; -import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { generateUuid } from 'vs/base/common/uuid'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -23,10 +23,11 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; -import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; +import { IViewBadge, IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; import { IOverlayWebview, IWebviewService, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewWindowDragMonitor } from 'vs/workbench/contrib/webview/browser/webviewWindowDragMonitor'; import { IWebviewViewService, WebviewView } from 'vs/workbench/contrib/webviewView/browser/webviewViewService'; +import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; declare const ResizeObserver: any; @@ -48,6 +49,9 @@ export class WebviewViewPane extends ViewPane { private readonly defaultTitle: string; private setTitle: string | undefined; + private badge: IViewBadge | undefined; + private activity: IDisposable | undefined; + private readonly memento: Memento; private readonly viewState: MementoObject; private readonly extensionId?: ExtensionIdentifier; @@ -69,6 +73,7 @@ export class WebviewViewPane extends ViewPane { @IWebviewService private readonly webviewService: IWebviewService, @IWebviewViewService private readonly webviewViewService: IWebviewViewService, @IViewsService private readonly viewService: IViewsService, + @IActivityService private activityService: IActivityService ) { super({ ...options, titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.extensionId = options.fromExtensionId; @@ -211,6 +216,9 @@ export class WebviewViewPane extends ViewPane { get description(): string | undefined { return self.titleDescription; }, set description(value: string | undefined) { self.updateTitleDescription(value); }, + get badge(): IViewBadge | undefined { return self.badge; }, + set badge(badge: IViewBadge | undefined) { self.updateBadge(badge); }, + dispose: () => { // Only reset and clear the webview itself. Don't dispose of the view container this._activated = false; @@ -232,6 +240,28 @@ export class WebviewViewPane extends ViewPane { super.updateTitle(typeof value === 'string' ? value : this.defaultTitle); } + protected updateBadge(badge: IViewBadge | undefined) { + + if (this.badge?.value === badge?.value && + this.badge?.tooltip === badge?.tooltip) { + return; + } + + if (this.activity) { + this.activity.dispose(); + this.activity = undefined; + } + + this.badge = badge; + if (badge) { + const activity = { + badge: new NumberBadge(badge.value, () => badge.tooltip), + priority: 150 + }; + this.activityService.showViewActivity(this.id, activity); + } + } + private async withProgress(task: () => Promise<void>): Promise<void> { return this.progressService.withProgress({ location: this.id, delay: 500 }, task); } diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts index c3565f919d5..c4ebff8bfee 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts @@ -7,6 +7,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IViewBadge } from 'vs/workbench/common/views'; import { IOverlayWebview } from 'vs/workbench/contrib/webview/browser/webview'; export const IWebviewViewService = createDecorator<IWebviewViewService>('webviewViewService'); @@ -14,6 +15,7 @@ export const IWebviewViewService = createDecorator<IWebviewViewService>('webview export interface WebviewView { title?: string; description?: string; + badge?: IViewBadge; readonly webview: IOverlayWebview; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts b/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts index e8339183da5..d73cfe79d1c 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts @@ -23,7 +23,7 @@ suite('Getting Started Markdown Renderer', () => { const rendered = await renderer.renderMarkdown(mdPath, mdBase); const imageSrcs = [...rendered.matchAll(/img src="[^"]*"/g)].map(match => match[0]); for (const src of imageSrcs) { - const targetSrcFormat = /^img src="https:\/\/file%2B.vscode-resource.vscode-webview.net\/.*\/vs\/workbench\/contrib\/welcomeGettingStarted\/common\/media\/.*.png"$/; + const targetSrcFormat = /^img src="https:\/\/file%2B.vscode-resource.vscode-cdn.net\/.*\/vs\/workbench\/contrib\/welcomeGettingStarted\/common\/media\/.*.png"$/; assert(targetSrcFormat.test(src), `${src} didnt match regex`); } languageService.dispose(); diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index e4c63f35ce9..6a78e65d27c 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -43,7 +43,6 @@ import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { NativeLogService } from 'vs/workbench/services/log/electron-sandbox/logService'; import { WorkspaceTrustEnablementService, WorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/common/workspaceTrust'; import { IWorkspaceTrustEnablementService, IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; -import { registerWindowDriver } from 'vs/platform/driver/electron-sandbox/driver'; import { safeStringify } from 'vs/base/common/objects'; import { ISharedProcessWorkerWorkbenchService, SharedProcessWorkerWorkbenchService } from 'vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessWorkerWorkbenchService'; import { isCI, isMacintosh } from 'vs/base/common/platform'; @@ -115,11 +114,6 @@ export class DesktopMain extends Disposable { // Window this._register(instantiationService.createInstance(NativeWindow)); - - // Driver - if (this.configuration.driver) { - instantiationService.invokeFunction(async accessor => this._register(await registerWindowDriver(accessor, this.configuration.windowId))); - } } private getExtraClasses(): string[] { diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 816fe88f29e..389c1218e67 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -63,6 +63,7 @@ import { whenEditorClosed } from 'vs/workbench/browser/editor'; import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { registerLegacyWindowDriver, registerWindowDriver } from 'vs/platform/driver/electron-sandbox/driver'; export class NativeWindow extends Disposable { @@ -570,6 +571,29 @@ export class NativeWindow extends Disposable { this.nativeHostService.openDevTools(); } } + + // Smoke Test Driver + this.setupDriver(); + } + + private setupDriver(): void { + + // Browser Driver + if (this.environmentService.args['enable-smoke-test-driver']) { + const that = this; + registerWindowDriver({ + async exitApplication(): Promise<number> { + that.nativeHostService.quit(); + + return that.environmentService.mainPid; + } + }); + } + + // Legacy Driver (TODO@bpasero remove me eventually) + else if (this.environmentService.args.driver) { + this.instantiationService.invokeFunction(async accessor => this._register(await registerLegacyWindowDriver(accessor, this.nativeHostService.windowId))); + } } private setupOpenHandlers(): void { 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 42eabddd8e2..014d9f42c43 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 @@ -631,6 +631,16 @@ suite('Configuration Resolver Service', () => { }); }); }); + + test('resolveWithEnvironment', () => { + const env = { + 'VAR_1': 'VAL_1', + 'VAR_2': 'VAL_2' + }; + const configuration = 'echo ${env:VAR_1}${env:VAR_2}'; + const resolvedResult = configurationResolverService!.resolveWithEnvironment({ ...env }, undefined, configuration); + assert.deepStrictEqual(resolvedResult, 'echo VAL_1VAL_2'); + }); }); diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index 8fba1ff2934..eb895e4de5e 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -29,6 +29,7 @@ import { PLAINTEXT_EXTENSION } from 'vs/editor/common/languages/modesRegistry'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EditorOpenSource } from 'vs/platform/editor/common/editor'; export abstract class AbstractFileDialogService implements IFileDialogService { @@ -172,7 +173,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService { if (stat.isDirectory || options.forceNewWindow || preferNewWindow) { await this.hostService.openWindow([toOpen], { forceNewWindow: options.forceNewWindow, remoteAuthority: options.remoteAuthority }); } else { - await this.openerService.open(uri, { fromUserGesture: true, editorOptions: { pinned: true } }); + await this.editorService.openEditors([{ resource: uri, options: { source: EditorOpenSource.USER, pinned: true } }], undefined, { validateTrust: true }); } } } @@ -188,7 +189,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService { if (options.forceNewWindow || preferNewWindow) { await this.hostService.openWindow([{ fileUri: uri }], { forceNewWindow: options.forceNewWindow, remoteAuthority: options.remoteAuthority }); } else { - await this.openerService.open(uri, { fromUserGesture: true, editorOptions: { pinned: true } }); + await this.editorService.openEditors([{ resource: uri, options: { source: EditorOpenSource.USER, pinned: true } }], undefined, { validateTrust: true }); } } } diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index c0aba429a3e..bdea82e86be 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -640,9 +640,11 @@ export interface IEditorGroup { * Closes specific editors in this group. This may trigger a confirmation dialog if * there are dirty editors and thus returns a promise as value. * - * @returns a promise when all editors are closed. + * @returns a promise whether the editors were closed or not. If `true`, the editors + * were closed and if `false` there was a veto closing the editors, e.g. when one + * is dirty. */ - closeEditors(editors: EditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise<void>; + closeEditors(editors: EditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise<boolean>; /** * Closes all editors from the group. This may trigger a confirmation dialog if diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 8531394ea3f..c52507b6659 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, ITestInstantiationService, TestServiceAccessor, createEditorPart } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, TestServiceAccessor, createEditorPart } from 'vs/workbench/test/browser/workbenchTestServices'; import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CloseDirection, IEditorPartOptions, EditorsOrder, EditorInputCapabilities, GroupModelChangeKind, SideBySideEditor } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; @@ -16,6 +16,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { IGroupModelChangeEvent, IGroupEditorMoveEvent, IGroupEditorOpenEvent } from 'vs/workbench/common/editor/editorGroupModel'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; suite('EditorGroupsService', () => { @@ -32,7 +33,7 @@ suite('EditorGroupsService', () => { disposables.clear(); }); - async function createPart(instantiationService = workbenchInstantiationService(undefined, disposables)): Promise<[TestEditorPart, ITestInstantiationService]> { + async function createPart(instantiationService = workbenchInstantiationService(undefined, disposables)): Promise<[TestEditorPart, TestInstantiationService]> { const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); @@ -609,6 +610,7 @@ suite('EditorGroupsService', () => { const accessor = instantiationService.createInstance(TestServiceAccessor); accessor.fileDialogService.setConfirmResult(ConfirmResult.DONT_SAVE); + let closeResult = false; const group = part.activeGroup; @@ -621,13 +623,15 @@ suite('EditorGroupsService', () => { await group.openEditor(input2); accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); - await group.closeEditors([input1, input2]); + closeResult = await group.closeEditors([input1, input2]); + assert.strictEqual(closeResult, false); assert.ok(!input1.gotDisposed); assert.ok(!input2.gotDisposed); accessor.fileDialogService.setConfirmResult(ConfirmResult.DONT_SAVE); - await group.closeEditors([input1, input2]); + closeResult = await group.closeEditors([input1, input2]); + assert.strictEqual(closeResult, true); assert.ok(input1.gotDisposed); assert.ok(input2.gotDisposed); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index 693c94059a6..478a9c0fa50 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -1555,7 +1555,7 @@ suite('EditorService', () => { }); test('openEditors() extracts proper resources from untyped editors for workspace trust', async () => { - const [part, service, accessor] = await createEditorService(); + const [, service, accessor] = await createEditorService(); const input = { resource: URI.parse('my://resource-openEditors') }; const otherInput: IResourceDiffEditorInput = { @@ -1573,7 +1573,6 @@ suite('EditorService', () => { }; await service.openEditors([input, otherInput], undefined, { validateTrust: true }); - assert.strictEqual(part.activeGroup.count, 0); assert.strictEqual(trustEditorUris.length, 3); assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input.resource.toString()), true); assert.strictEqual(trustEditorUris.some(uri => uri.toString() === otherInput.original.resource?.toString()), true); @@ -1999,22 +1998,27 @@ suite('EditorService', () => { assert.strictEqual(service.activeTextEditorLanguageId, PLAINTEXT_LANGUAGE_ID); }); - test('openEditor returns NULL when opening fails or is inactive', async function () { + test('openEditor returns undefined when inactive', async function () { const [, service] = await createEditorService(); const input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); const otherInput = new TestFileEditorInput(URI.parse('my://resource2-inactive'), TEST_EDITOR_INPUT_ID); - const failingInput = new TestFileEditorInput(URI.parse('my://resource3-failing'), TEST_EDITOR_INPUT_ID); - failingInput.setFailToOpen(); let editor = await service.openEditor(input, { pinned: true }); assert.ok(editor); let otherEditor = await service.openEditor(otherInput, { inactive: true }); assert.ok(!otherEditor); + }); + + test('openEditor shows placeholder when opening fails', async function () { + const [, service] = await createEditorService(); + + const failingInput = new TestFileEditorInput(URI.parse('my://resource-failing'), TEST_EDITOR_INPUT_ID); + failingInput.setFailToOpen(); let failingEditor = await service.openEditor(failingInput); - assert.ok(!failingEditor); + assert.ok(failingEditor instanceof UnknownErrorEditor); }); test('openEditor shows placeholder when restoring fails', async function () { diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 0028c843a15..aec5f09e2ec 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -181,7 +181,7 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi get webviewExternalEndpoint(): string { const endpoint = this.options.webviewEndpoint || this.productService.webviewContentExternalBaseUrlTemplate - || 'https://{{uuid}}.vscode-webview.net/{{quality}}/{{commit}}/out/vs/workbench/contrib/webview/browser/pre/'; + || 'https://{{uuid}}.vscode-cdn.net/{{quality}}/{{commit}}/out/vs/workbench/contrib/webview/browser/pre/'; const webviewExternalEndpointCommit = this.payload?.get('webviewExternalEndpointCommit'); return endpoint diff --git a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts index 00076bb9cc3..9de89527a82 100644 --- a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts @@ -8,59 +8,26 @@ import * as json from 'vs/base/common/json'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ChordKeybinding, SimpleKeybinding } from 'vs/base/common/keybindings'; import { OS } from 'vs/base/common/platform'; -import { ILanguageService } from 'vs/editor/common/languages/language'; -import { LanguageService } from 'vs/editor/common/services/languageService'; -import { IModelService } from 'vs/editor/common/services/model'; -import { ModelService } from 'vs/editor/common/services/modelService'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; -import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { ILogService, NullLogService } from 'vs/platform/log/common/log'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { NullLogService } from 'vs/platform/log/common/log'; import { KeybindingsEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; -import { TestWorkingCopyBackupService, TestEditorGroupsService, TestEditorService, TestEnvironmentService, TestLifecycleService, TestPathService, TestTextFileService, TestDecorationsService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEnvironmentService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { IWorkingCopyService, WorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { LabelService } from 'vs/workbench/services/label/common/labelService'; -import { IFilesConfigurationService, FilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { WorkingCopyFileService, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; -import { TestTextResourcePropertiesService, TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { joinPath } from 'vs/base/common/resources'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IDecorationsService } from 'vs/workbench/services/decorations/common/decorations'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; interface Modifiers { metaKey?: boolean; @@ -74,56 +41,34 @@ const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); suite('KeybindingsEditing', () => { const disposables = new DisposableStore(); - let instantiationService: TestInstantiationService, fileService: IFileService, environmentService: IEnvironmentService; + let instantiationService: TestInstantiationService; + let fileService: IFileService; + let environmentService: IEnvironmentService; let testObject: KeybindingsEditingService; setup(async () => { + + environmentService = TestEnvironmentService; + const logService = new NullLogService(); fileService = disposables.add(new FileService(logService)); const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); + disposables.add(fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService())))); const userFolder = joinPath(ROOT, 'User'); await fileService.createFolder(userFolder); - environmentService = TestEnvironmentService; - - instantiationService = new TestInstantiationService(); const configService = new TestConfigurationService(); configService.setUserConfiguration('files', { 'eol': '\n' }); - instantiationService.stub(IEnvironmentService, environmentService); - instantiationService.stub(IDecorationsService, TestDecorationsService); - instantiationService.stub(IWorkbenchEnvironmentService, environmentService); - instantiationService.stub(IPathService, new TestPathService()); - instantiationService.stub(IConfigurationService, configService); - instantiationService.stub(IWorkspaceContextService, new TestContextService()); - const lifecycleService = new TestLifecycleService(); - instantiationService.stub(ILifecycleService, lifecycleService); - instantiationService.stub(IContextKeyService, <IContextKeyService>instantiationService.createInstance(MockContextKeyService)); - instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService()); - instantiationService.stub(IEditorService, new TestEditorService()); - instantiationService.stub(IWorkingCopyService, disposables.add(new WorkingCopyService())); - instantiationService.stub(ITelemetryService, NullTelemetryService); - instantiationService.stub(ILanguageService, LanguageService); - instantiationService.stub(ILogService, new NullLogService()); - instantiationService.stub(ILabelService, disposables.add(instantiationService.createInstance(LabelService))); - instantiationService.stub(IFilesConfigurationService, disposables.add(instantiationService.createInstance(FilesConfigurationService))); - instantiationService.stub(ITextResourcePropertiesService, new TestTextResourcePropertiesService(instantiationService.get(IConfigurationService))); - instantiationService.stub(IUndoRedoService, instantiationService.createInstance(UndoRedoService)); - instantiationService.stub(IThemeService, new TestThemeService()); - instantiationService.stub(ILanguageConfigurationService, new TestLanguageConfigurationService()); - instantiationService.stub(IModelService, disposables.add(instantiationService.createInstance(ModelService))); - fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - instantiationService.stub(IFileService, fileService); - instantiationService.stub(IUriIdentityService, new UriIdentityService(fileService)); - instantiationService.stub(IWorkingCopyFileService, disposables.add(instantiationService.createInstance(WorkingCopyFileService))); - instantiationService.stub(ITextFileService, disposables.add(instantiationService.createInstance(TestTextFileService))); - instantiationService.stub(ITextModelService, disposables.add(instantiationService.createInstance(TextModelResolverService))); - instantiationService.stub(IWorkingCopyBackupService, new TestWorkingCopyBackupService()); + instantiationService = workbenchInstantiationService({ + fileService: () => fileService, + configurationService: () => configService, + environmentService: () => environmentService + }, disposables); testObject = disposables.add(instantiationService.createInstance(KeybindingsEditingService)); - }); teardown(() => disposables.clear()); @@ -318,5 +263,4 @@ suite('KeybindingsEditing', () => { const keybinding = parts.length > 0 ? new USLayoutResolvedKeybinding(new ChordKeybinding(parts), OS) : undefined; return new ResolvedKeybindingItem(keybinding, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : undefined, isDefault === undefined ? true : isDefault, null, false); } - }); diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 7a92fa6e6fa..72f92bf15c0 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -42,6 +42,7 @@ import { IDecorationData, IDecorationsProvider, IDecorationsService } from 'vs/w import { Emitter } from 'vs/base/common/event'; import { Codicon } from 'vs/base/common/codicons'; import { listErrorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { withNullAsUndefined } from 'vs/base/common/types'; /** * The workbench file service implementation implements the raw file service spec and adds additional methods on top. @@ -305,7 +306,11 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return toDecodeStream(stream, { acceptTextOnly: options?.acceptTextOnly ?? false, guessEncoding: options?.autoGuessEncoding || this.textResourceConfigurationService.getValue(resource, 'files.autoGuessEncoding'), - overwriteEncoding: detectedEncoding => this.encoding.getReadEncoding(resource, options, detectedEncoding) + overwriteEncoding: async detectedEncoding => { + const { encoding } = await this.encoding.getPreferredReadEncoding(resource, options, withNullAsUndefined(detectedEncoding)); + + return encoding; + } }); } @@ -731,7 +736,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings { }; } - getReadEncoding(resource: URI, options: IReadTextFileEncodingOptions | undefined, detectedEncoding: string | null): Promise<string> { + async getPreferredReadEncoding(resource: URI, options?: IReadTextFileEncodingOptions, detectedEncoding?: string): Promise<IResourceEncoding> { let preferredEncoding: string | undefined; // Encoding passed in as option @@ -744,7 +749,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings { } // Encoding detected - else if (detectedEncoding) { + else if (typeof detectedEncoding === 'string') { preferredEncoding = detectedEncoding; } @@ -753,7 +758,12 @@ export class EncodingOracle extends Disposable implements IResourceEncodings { preferredEncoding = UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then } - return this.getEncodingForResource(resource, preferredEncoding); + const encoding = await this.getEncodingForResource(resource, preferredEncoding); + + return { + encoding, + hasBOM: encoding === UTF16be || encoding === UTF16le || encoding === UTF8_with_bom // enforce BOM for certain encodings + }; } private async getEncodingForResource(resource: URI, preferredEncoding?: string): Promise<string> { diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index da6f65d79f8..02493a12664 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; import { EncodingMode, ITextFileService, TextFileEditorModelState, ITextFileEditorModel, ITextFileStreamContent, ITextFileResolveOptions, IResolvedTextFileEditorModel, ITextFileSaveOptions, TextFileResolveReason, ITextFileEditorModelSaveEvent } from 'vs/workbench/services/textfile/common/textfiles'; -import { IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; +import { IRevertOptions, SaveReason, SaveSourceRegistry } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IWorkingCopyBackupService, IResolvedWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IFileService, FileOperationError, FileOperationResult, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED, FileSystemProviderCapabilities, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; @@ -44,6 +45,8 @@ interface IBackupMetaData extends IWorkingCopyBackupMeta { */ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFileEditorModel { + private static readonly TEXTFILE_SAVE_ENCODING_SOURCE = SaveSourceRegistry.registerSource('textFileEncoding.source', localize('textFileCreate.source', "File Encoding Changed")); + //#region Events private readonly _onDidChangeContent = this._register(new Emitter<void>()); @@ -116,7 +119,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil @ILanguageDetectionService languageDetectionService: ILanguageDetectionService, @IAccessibilityService accessibilityService: IAccessibilityService, @IPathService private readonly pathService: IPathService, - @IExtensionService private readonly extensionService: IExtensionService, + @IExtensionService private readonly extensionService: IExtensionService ) { super(modelService, languageService, languageDetectionService, accessibilityService); @@ -128,7 +131,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private registerListeners(): void { this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); - this._register(this.filesConfigurationService.onFilesAssociationChange(e => this.onFilesAssociationChange())); + this._register(this.filesConfigurationService.onFilesAssociationChange(() => this.onFilesAssociationChange())); } private async onDidFilesChange(e: FileChangesEvent): Promise<void> { @@ -241,7 +244,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil const softUndo = options?.soft; if (!softUndo) { try { - await this.resolve({ forceReadFromFile: true }); + await this.forceResolveFromFile(); } catch (error) { // FileNotFound means the file got deleted meanwhile, so ignore it @@ -559,8 +562,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // This code has been extracted to a different method because it caused a memory leak // where `value` was captured in the content change listener closure scope. - // Content Change + // Listen to text model events this._register(model.onDidChangeContent(e => this.onModelContentChanged(model, e.isUndoing || e.isRedoing))); + this._register(model.onDidChangeLanguage(e => this.onMaybeShouldChangeEncoding())); // detect possible encoding change via language specific settings } private onModelContentChanged(model: ITextModel, isUndoingOrRedoing: boolean): void { @@ -613,19 +617,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.autoDetectLanguage(); } - private static _whenReadyToDetectLanguage: Promise<boolean> | undefined; - private whenReadyToDetectLanguage(): Promise<boolean> { - if (TextFileEditorModel._whenReadyToDetectLanguage === undefined) { - // We need to wait until installed extensions are registered because if the editor is created before that (like when restoring an editor) - // it could be created with a language of plaintext. This would trigger language detection even though the real issue is that the - // language extensions are not yet loaded to provide the actual language. - TextFileEditorModel._whenReadyToDetectLanguage = this.extensionService.whenInstalledExtensionsRegistered(); - } - return TextFileEditorModel._whenReadyToDetectLanguage; - } - protected override async autoDetectLanguage(): Promise<void> { - await this.whenReadyToDetectLanguage(); + + // Wait to be ready to detect language + await this.extensionService?.whenInstalledExtensionsRegistered(); + + // Only perform language detection conditionally const languageId = this.getLanguageId(); if ( this.resource.scheme === this.pathService.defaultUriScheme && // make sure to not detect language for non-user visible documents @@ -636,6 +633,23 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } + private async forceResolveFromFile(): Promise<void> { + if (this.isDisposed()) { + return; // return early when the model is invalid + } + + // We go through the text file service to make + // sure this kind of `resolve` is properly + // running in sequence with any other running + // `resolve` if any, including subsequent runs + // that are triggered right after. + + await this.textFileService.files.resolve(this.resource, { + reload: { async: false }, + forceReadFromFile: true + }); + } + //#endregion //#region Dirty @@ -986,15 +1000,55 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#region Encoding - getEncoding(): string | undefined { - return this.preferredEncoding || this.contentEncoding; - } + private async onMaybeShouldChangeEncoding(): Promise<void> { - async setEncoding(encoding: string, mode: EncodingMode): Promise<void> { - if (!this.isNewEncoding(encoding)) { - return; // return early if the encoding is already the same + // This is a bit of a hack but there is a narrow case where + // per-language configured encodings are not working: + // + // On startup we may not yet have all languages resolved so + // we pick a wrong encoding. We never used to re-apply the + // encoding when the language was then resolved, because that + // is an operation that is will have to fetch the contents + // again from disk. + // + // To mitigate this issue, when we detect the model language + // changes, we see if there is a specific encoding configured + // for the new language and apply it, only if the model is + // not dirty and only if the encoding was not explicitly set. + // + // (see https://github.com/microsoft/vscode/issues/127936) + + if (this.hasEncodingSetExplicitly) { + return; // never change the user's choice of encoding } + const { encoding } = await this.textFileService.encoding.getPreferredReadEncoding(this.resource); + if (typeof encoding !== 'string' || !this.isNewEncoding(encoding)) { + return; // return early if encoding is invalid or did not change + } + + if (this.isDirty()) { + return; // return early to prevent accident saves in this case + } + + this.logService.info(`Adjusting encoding based on configured language override to '${encoding}' for ${this.resource.toString(true)}.`); + + // Re-open with new encoding + return this.setEncodingInternal(encoding, EncodingMode.Decode); + } + + private hasEncodingSetExplicitly: boolean = false; + + setEncoding(encoding: string, mode: EncodingMode): Promise<void> { + + // Remember that an explicit encoding was set + this.hasEncodingSetExplicitly = true; + + return this.setEncodingInternal(encoding, mode); + } + + private async setEncodingInternal(encoding: string, mode: EncodingMode): Promise<void> { + // Encode: Save with encoding if (mode === EncodingMode.Encode) { this.updatePreferredEncoding(encoding); @@ -1006,21 +1060,23 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } if (!this.inConflictMode) { - await this.save(); + await this.save({ source: TextFileEditorModel.TEXTFILE_SAVE_ENCODING_SOURCE }); } } // Decode: Resolve with encoding else { - if (this.isDirty()) { + if (!this.isNewEncoding(encoding)) { + return; // return early if the encoding is already the same + } + + if (this.isDirty() && !this.inConflictMode) { await this.save(); } this.updatePreferredEncoding(encoding); - await this.resolve({ - forceReadFromFile: true // because encoding has changed - }); + await this.forceResolveFromFile(); } } @@ -1047,6 +1103,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return true; } + getEncoding(): string | undefined { + return this.preferredEncoding || this.contentEncoding; + } + //#endregion private trace(msg: string): void { diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 987c31e09c2..8cb717b2fca 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -178,6 +178,7 @@ export class TextFileOperationError extends FileOperationError { } export interface IResourceEncodings { + getPreferredReadEncoding(resource: URI): Promise<IResourceEncoding>; getPreferredWriteEncoding(resource: URI, preferredEncoding?: string): Promise<IResourceEncoding>; } @@ -265,12 +266,7 @@ export interface ITextFileStreamContent extends IBaseTextFileContent { readonly value: ITextBufferFactory; } -export interface ITextFileEditorModelResolveOrCreateOptions { - - /** - * Context why the model is being resolved or created. - */ - readonly reason?: TextFileResolveReason; +export interface ITextFileEditorModelResolveOrCreateOptions extends ITextFileResolveOptions { /** * The language id to use for the model text content. @@ -283,13 +279,6 @@ export interface ITextFileEditorModelResolveOrCreateOptions { readonly encoding?: string; /** - * The contents to use for the model if known. If not - * provided, the contents will be retrieved from the - * underlying resource or backup if present. - */ - readonly contents?: ITextBufferFactory; - - /** * If the model was already resolved before, allows to trigger * a reload of it to fetch the latest contents. */ @@ -301,11 +290,6 @@ export interface ITextFileEditorModelResolveOrCreateOptions { */ readonly async: boolean; }; - - /** - * Allow to resolve a model even if we think it is a binary file. - */ - readonly allowBinary?: boolean; } export interface ITextFileSaveEvent extends ITextFileEditorModelSaveEvent { diff --git a/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts b/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts index 9f775d700aa..f29fd72d650 100644 --- a/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts @@ -14,7 +14,6 @@ import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/u import { toResource } from 'vs/base/test/common/utils'; import { IFileService } from 'vs/platform/files/common/files'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; @@ -22,6 +21,7 @@ import { isLinux } from 'vs/base/common/platform'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { TextEditorService } from 'vs/workbench/services/textfile/common/textEditorService'; +import { ILanguageService } from 'vs/editor/common/languages/language'; suite('TextEditorService', () => { @@ -50,10 +50,11 @@ suite('TextEditorService', () => { test('createTextEditor - basics', async function () { const instantiationService = workbenchInstantiationService(undefined, disposables); + const languageService = instantiationService.get(ILanguageService); const service = instantiationService.createInstance(TextEditorService); const languageId = 'create-input-test'; - ModesRegistry.registerLanguage({ + const registration = languageService.registerLanguage({ id: languageId, }); @@ -182,6 +183,8 @@ suite('TextEditorService', () => { const untypedSideBySideInput = input.toUntyped() as IResourceSideBySideEditorInput; assert.strictEqual(untypedSideBySideInput.primary.resource?.toString(), sideBySideResourceInput.primary.resource.toString()); assert.strictEqual(untypedSideBySideInput.secondary.resource?.toString(), sideBySideResourceInput.secondary.resource.toString()); + + registration.dispose(); }); test('createTextEditor- caching', function () { diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index 21bcf191cc5..9f6d6e6fd0a 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -11,8 +11,7 @@ import { createFileEditorInput, workbenchInstantiationService, TestServiceAccess import { toResource } from 'vs/base/test/common/utils'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; -import { timeout } from 'vs/base/common/async'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; +import { DeferredPromise, timeout } from 'vs/base/common/async'; import { assertIsDefined } from 'vs/base/common/types'; import { createTextBufferFactory, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -20,6 +19,8 @@ import { URI } from 'vs/base/common/uri'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { SaveReason, SaveSourceRegistry } from 'vs/workbench/common/editor'; +import { isEqual } from 'vs/base/common/resources'; +import { UTF16be } from 'vs/workbench/services/textfile/common/encoding'; suite('Files - TextFileEditorModel', () => { @@ -49,6 +50,7 @@ suite('Files - TextFileEditorModel', () => { test('basic events', async function () { const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + accessor.workingCopyService.unregisterWorkingCopy(model); // causes issues with subsequent resolves otherwise let onDidResolveCounter = 0; model.onDidResolve(() => onDidResolveCounter++); @@ -300,16 +302,24 @@ suite('Files - TextFileEditorModel', () => { }); test('setEncoding - decode', async function () { - const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + accessor.workingCopyService.unregisterWorkingCopy(model); // causes issues with subsequent resolves otherwise await model.setEncoding('utf16', EncodingMode.Decode); + // we have to get the model again from working copy service + // because `setEncoding` will resolve it again through the + // text file service which is outside our scope + model = accessor.workingCopyService.get(model) as TextFileEditorModel; + assert.ok(model.isResolved()); // model got resolved due to decoding model.dispose(); }); test('setEncoding - decode dirty file saves first', async function () { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + accessor.workingCopyService.unregisterWorkingCopy(model); // causes issues with subsequent resolves otherwise + await model.resolve(); model.updateTextEditorModel(createTextBufferFactory('bar')); @@ -321,9 +331,45 @@ suite('Files - TextFileEditorModel', () => { model.dispose(); }); + test('encoding updates with language based configuration', async function () { + const languageId = 'text-file-model-test'; + const registration = accessor.languageService.registerLanguage({ + id: languageId, + }); + + accessor.testConfigurationService.setOverrideIdentifiers('files.encoding', [languageId]); + + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + accessor.workingCopyService.unregisterWorkingCopy(model); // causes issues with subsequent resolves otherwise + + await model.resolve(); + + const deferredPromise = new DeferredPromise<TextFileEditorModel>(); + + // We use this listener as a way to figure out that the working + // copy was resolved again as part of the language change + const listener = accessor.workingCopyService.onDidRegister(e => { + if (isEqual(e.resource, model.resource)) { + deferredPromise.complete(model as TextFileEditorModel); + } + }); + + accessor.testConfigurationService.setUserConfiguration('files.encoding', UTF16be); + + model.setLanguageId(languageId); + + await deferredPromise.p; + + assert.strictEqual(model.getEncoding(), UTF16be); + + model.dispose(); + listener.dispose(); + registration.dispose(); + }); + test('create with language', async function () { const languageId = 'text-file-model-test'; - ModesRegistry.registerLanguage({ + const registration = accessor.languageService.registerLanguage({ id: languageId, }); @@ -335,6 +381,8 @@ suite('Files - TextFileEditorModel', () => { model.dispose(); assert.ok(!accessor.modelService.getModel(model.resource)); + + registration.dispose(); }); test('disposes when underlying model is destroyed', async function () { @@ -397,7 +445,7 @@ suite('Files - TextFileEditorModel', () => { test('Revert', async function () { let eventCounter = 0; - const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + let model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); model.onDidRevert(() => eventCounter++); @@ -415,9 +463,16 @@ suite('Files - TextFileEditorModel', () => { assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); + accessor.workingCopyService.unregisterWorkingCopy(model); // causes issues with subsequent resolves otherwise + await model.revert(); + + // we have to get the model again from working copy service + // because `setEncoding` will resolve it again through the + // text file service which is outside our scope + model = accessor.workingCopyService.get(model) as TextFileEditorModel; + assert.strictEqual(model.isDirty(), false); - assert.strictEqual(model.textEditorModel!.getValue(), 'Hello Html'); assert.strictEqual(eventCounter, 1); assert.ok(workingCopyEvent); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts index dca729de1b5..a742cab97a6 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts @@ -6,12 +6,11 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { workbenchInstantiationService, TestServiceAccessor, TestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { toResource } from 'vs/base/test/common/utils'; -import { ModesRegistry, PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { timeout } from 'vs/base/common/async'; @@ -34,7 +33,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('add, remove, clear, get, getAll', function () { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined); const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined); @@ -91,7 +90,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('resolve', async () => { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const resource = URI.file('/test.html'); const encoding = 'utf8'; @@ -131,7 +130,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('resolve (async)', async () => { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const resource = URI.file('/path/index.txt'); await manager.resolve(resource); @@ -154,7 +153,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('resolve (sync)', async () => { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const resource = URI.file('/path/index.txt'); await manager.resolve(resource); @@ -171,7 +170,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('resolve (sync) - model disposed when error and first call to resolve', async () => { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const resource = URI.file('/path/index.txt'); accessor.textFileService.setReadStreamErrorOnce(new FileOperationError('fail', FileOperationResult.FILE_OTHER_ERROR)); @@ -188,7 +187,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('resolve (sync) - model not disposed when error and model existed before', async () => { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const resource = URI.file('/path/index.txt'); await manager.resolve(resource); @@ -207,7 +206,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('resolve with initial contents', async () => { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const resource = URI.file('/test.html'); const model = await manager.resolve(resource, { contents: createTextBufferFactory('Hello World') }); @@ -223,7 +222,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('multiple resolves execute in sequence', async () => { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const resource = URI.file('/test.html'); let resolvedModel: unknown; @@ -257,7 +256,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('removed from cache when model disposed', function () { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined); const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined); @@ -279,7 +278,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('events', async function () { - const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const resource1 = toResource.call(this, '/path/index.txt'); const resource2 = toResource.call(this, '/path/other.txt'); @@ -368,7 +367,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('disposing model takes it out of the manager', async function () { - const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const resource = toResource.call(this, '/path/index_something.txt'); @@ -380,7 +379,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('canDispose with dirty model', async function () { - const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const resource = toResource.call(this, '/path/index_something.txt'); @@ -409,12 +408,13 @@ suite('Files - TextFileEditorModelManager', () => { }); test('language', async function () { + const languageId = 'text-file-model-manager-test'; - ModesRegistry.registerLanguage({ + const registration = accessor.languageService.registerLanguage({ id: languageId, }); - const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const resource = toResource.call(this, '/path/index_something.txt'); @@ -426,10 +426,11 @@ suite('Files - TextFileEditorModelManager', () => { model.dispose(); manager.dispose(); + registration.dispose(); }); test('file change events trigger reload (on a resolved model)', async () => { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const resource = URI.file('/path/index.txt'); await manager.resolve(resource); @@ -451,7 +452,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('file change events trigger reload (after a model is resolved: https://github.com/microsoft/vscode/issues/132765)', async () => { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager = accessor.textFileService.files as TestTextFileEditorModelManager; const resource = URI.file('/path/index.txt'); manager.resolve(resource); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts index b31f6da3be2..6bd19ad2707 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts @@ -9,7 +9,6 @@ import { toResource } from 'vs/base/test/common/utils'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { FileOperation } from 'vs/platform/files/common/files'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; import { DisposableStore } from 'vs/base/common/lifecycle'; suite('Files - TextFileService', () => { @@ -139,17 +138,18 @@ suite('Files - TextFileService', () => { }); test('Filename Suggestion - Suggest prefix only when there are no relevant extensions', () => { - ModesRegistry.registerLanguage({ + const registration = accessor.languageService.registerLanguage({ id: 'plumbus0', extensions: ['.one', '.two'] }); let suggested = accessor.textFileService.suggestFilename('shleem', 'Untitled-1'); assert.strictEqual(suggested, 'Untitled-1'); + registration.dispose(); }); test('Filename Suggestion - Suggest prefix with first extension', () => { - ModesRegistry.registerLanguage({ + const registration = accessor.languageService.registerLanguage({ id: 'plumbus1', extensions: ['.shleem', '.gazorpazorp'], filenames: ['plumbus'] @@ -157,15 +157,17 @@ suite('Files - TextFileService', () => { let suggested = accessor.textFileService.suggestFilename('plumbus1', 'Untitled-1'); assert.strictEqual(suggested, 'Untitled-1.shleem'); + registration.dispose(); }); test('Filename Suggestion - Suggest filename if there are no extensions', () => { - ModesRegistry.registerLanguage({ + const registration = accessor.languageService.registerLanguage({ id: 'plumbus2', filenames: ['plumbus', 'shleem', 'gazorpazorp'] }); let suggested = accessor.textFileService.suggestFilename('plumbus2', 'Untitled-1'); assert.strictEqual(suggested, 'plumbus'); + registration.dispose(); }); }); diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index 65c62f9c659..02eb92cc47c 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -125,7 +125,6 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt //#endregion - constructor( readonly resource: URI, readonly hasAssociatedFilePath: boolean, @@ -189,7 +188,6 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } } - //#region Language override setLanguageId(languageId: string): void { @@ -213,7 +211,6 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt //#endregion - //#region Encoding private configuredEncoding: string | undefined; @@ -234,7 +231,6 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt //#endregion - //#region Dirty private dirty = this.hasAssociatedFilePath || !!this.initialValue; @@ -254,7 +250,6 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt //#endregion - //#region Save / Revert / Backup async save(options?: ISaveOptions): Promise<boolean> { @@ -300,7 +295,6 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt //#endregion - //#region Resolve override async resolve(): Promise<void> { @@ -427,7 +421,6 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt //#endregion - override isReadonly(): boolean { return false; } diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index df21adbf510..da053c29e11 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -10,7 +10,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; -import { ModesRegistry, PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; @@ -291,7 +291,7 @@ suite('Untitled text editors', () => { test('can change language afterwards', async () => { const languageId = 'untitled-input-test'; - ModesRegistry.registerLanguage({ + const registration = accessor.languageService.registerLanguage({ id: languageId, }); @@ -309,12 +309,13 @@ suite('Untitled text editors', () => { input.dispose(); model.dispose(); + registration.dispose(); }); test('remembers that language was set explicitly', async () => { const language = 'untitled-input-test'; - ModesRegistry.registerLanguage({ + const registration = accessor.languageService.registerLanguage({ id: language, }); @@ -330,6 +331,7 @@ suite('Untitled text editors', () => { input.dispose(); model.dispose(); + registration.dispose(); }); test('service#onDidChangeEncoding', async () => { diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncEnablementService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncEnablementService.ts index bfc01daa46e..8c815116f35 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncEnablementService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncEnablementService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IUserDataSyncEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncEnablementService as BaseUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSyncEnablementService'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; @@ -17,46 +18,4 @@ export class UserDataSyncEnablementService extends BaseUserDataSyncEnablementSer } -export class WebUserDataSyncEnablementService extends UserDataSyncEnablementService implements IUserDataSyncEnablementService { - - private enabled: boolean | undefined = undefined; - - override canToggleEnablement(): boolean { - return this.isTrusted() && super.canToggleEnablement(); - } - - override isEnabled(): boolean { - if (!this.isTrusted()) { - return false; - } - if (this.enabled === undefined) { - this.enabled = this.workbenchEnvironmentService.options?.settingsSyncOptions?.enabled; - } - if (this.enabled === undefined) { - this.enabled = super.isEnabled(); - } - return this.enabled; - } - - override setEnablement(enabled: boolean) { - if (enabled && !this.canToggleEnablement()) { - return; - } - if (this.enabled !== enabled) { - this.enabled = enabled; - super.setEnablement(enabled); - if (this.workbenchEnvironmentService.options?.settingsSyncOptions?.enablementHandler) { - this.workbenchEnvironmentService.options.settingsSyncOptions.enablementHandler(this.enabled); - } - } - } - - override getResourceSyncStateVersion(resource: SyncResource): string | undefined { - return resource === SyncResource.Extensions ? this.workbenchEnvironmentService.options?.settingsSyncOptions?.extensionsSyncStateVersion : undefined; - } - - private isTrusted(): boolean { - return !!this.workbenchEnvironmentService.options?.workspaceProvider?.trusted; - } - -} +registerSingleton(IUserDataSyncEnablementService, UserDataSyncEnablementService); diff --git a/src/vs/workbench/services/userDataSync/browser/webUserDataSyncEnablementService.ts b/src/vs/workbench/services/userDataSync/browser/webUserDataSyncEnablementService.ts new file mode 100644 index 00000000000..1224476ffdc --- /dev/null +++ b/src/vs/workbench/services/userDataSync/browser/webUserDataSyncEnablementService.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IUserDataSyncEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncEnablementService } from 'vs/workbench/services/userDataSync/browser/userDataSyncEnablementService'; + +export class WebUserDataSyncEnablementService extends UserDataSyncEnablementService implements IUserDataSyncEnablementService { + + private enabled: boolean | undefined = undefined; + + override canToggleEnablement(): boolean { + return this.isTrusted() && super.canToggleEnablement(); + } + + override isEnabled(): boolean { + if (!this.isTrusted()) { + return false; + } + if (this.enabled === undefined) { + this.enabled = this.workbenchEnvironmentService.options?.settingsSyncOptions?.enabled; + } + if (this.enabled === undefined) { + this.enabled = super.isEnabled(); + } + return this.enabled; + } + + override setEnablement(enabled: boolean) { + if (enabled && !this.canToggleEnablement()) { + return; + } + if (this.enabled !== enabled) { + this.enabled = enabled; + super.setEnablement(enabled); + if (this.workbenchEnvironmentService.options?.settingsSyncOptions?.enablementHandler) { + this.workbenchEnvironmentService.options.settingsSyncOptions.enablementHandler(this.enabled); + } + } + } + + override getResourceSyncStateVersion(resource: SyncResource): string | undefined { + return resource === SyncResource.Extensions ? this.workbenchEnvironmentService.options?.settingsSyncOptions?.extensionsSyncStateVersion : undefined; + } + + private isTrusted(): boolean { + return !!this.workbenchEnvironmentService.options?.workspaceProvider?.trusted; + } + +} + +registerSingleton(IUserDataSyncEnablementService, WebUserDataSyncEnablementService); diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index 1f7853a9435..15b31899685 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -226,6 +226,16 @@ export interface IStoredFileWorkingCopySaveOptions extends ISaveOptions { readonly ignoreErrorHandler?: boolean; } +export interface IStoredFileWorkingCopyResolver { + + /** + * Resolves the working copy in a safe way from an external + * working copy manager that can make sure multiple parallel + * resolves execute properly. + */ + (options?: IStoredFileWorkingCopyResolveOptions): Promise<void>; +} + export interface IStoredFileWorkingCopyResolveOptions { /** @@ -306,6 +316,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend resource: URI, readonly name: string, private readonly modelFactory: IStoredFileWorkingCopyModelFactory<M>, + private readonly externalResolver: IStoredFileWorkingCopyResolver, @IFileService fileService: IFileService, @ILogService private readonly logService: ILogService, @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, @@ -716,6 +727,22 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend this._onDidChangeContent.fire(); } + private async forceResolveFromFile(): Promise<void> { + if (this.isDisposed()) { + return; // return early when the working copy is invalid + } + + // We go through the resolver to make + // sure this kind of `resolve` is properly + // running in sequence with any other running + // `resolve` if any, including subsequent runs + // that are triggered right after. + + await this.externalResolver({ + forceReadFromFile: true + }); + } + //#endregion //#region Backup @@ -1132,7 +1159,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend const softUndo = options?.soft; if (!softUndo) { try { - await this.resolve({ forceReadFromFile: true }); + await this.forceResolveFromFile(); } catch (error) { // FileNotFound means the file got deleted meanwhile, so ignore it diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts index 3fce10d67fa..b92776c6c5a 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts @@ -112,17 +112,7 @@ export interface IStoredFileWorkingCopySaveEvent<M extends IStoredFileWorkingCop readonly workingCopy: IStoredFileWorkingCopy<M>; } -export interface IStoredFileWorkingCopyManagerResolveOptions { - - /** - * The contents to use for the stored file working copy if known. If not - * provided, the contents will be retrieved from the underlying - * resource or backup if present. - * - * If contents are provided, the stored file working copy will be marked - * as dirty right from the beginning. - */ - readonly contents?: VSBufferReadableStream; +export interface IStoredFileWorkingCopyManagerResolveOptions extends IStoredFileWorkingCopyResolveOptions { /** * If the stored file working copy was already resolved before, @@ -529,6 +519,7 @@ export class StoredFileWorkingCopyManager<M extends IStoredFileWorkingCopyModel> resource, this.labelService.getUriBasenameLabel(resource), this.modelFactory, + async options => { await this.resolve(resource, { ...options, reload: { async: false } }); }, this.fileService, this.logService, this.workingCopyFileService, this.filesConfigurationService, this.workingCopyBackupService, this.workingCopyService, this.notificationService, this.workingCopyEditorService, this.editorService, this.elevatedFileService diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index e62fcc1fb1a..a7522cbcb77 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -193,7 +193,7 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic }); } - private unregisterWorkingCopy(workingCopy: IWorkingCopy): void { + protected unregisterWorkingCopy(workingCopy: IWorkingCopy): void { // Registry (all) this._workingCopies.delete(workingCopy); diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts index d7985656404..d8fccfa5189 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts @@ -99,7 +99,9 @@ suite('StoredFileWorkingCopy', function () { let workingCopy: StoredFileWorkingCopy<TestStoredFileWorkingCopyModel>; function createWorkingCopy(uri: URI = resource) { - return new StoredFileWorkingCopy<TestStoredFileWorkingCopyModel>('testStoredFileWorkingCopyType', uri, basename(uri), factory, accessor.fileService, accessor.logService, accessor.workingCopyFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService); + const workingCopy: StoredFileWorkingCopy<TestStoredFileWorkingCopyModel> = new StoredFileWorkingCopy<TestStoredFileWorkingCopyModel>('testStoredFileWorkingCopyType', uri, basename(uri), factory, options => workingCopy.resolve(options), accessor.fileService, accessor.logService, accessor.workingCopyFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService); + + return workingCopy; } setup(() => { diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts index 216c922bf5f..95ab3cb327c 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts @@ -273,7 +273,7 @@ suite('StoredFileWorkingCopyManager', () => { let savedCounter = 0; let saveErrorCounter = 0; - manager.onDidCreate(workingCopy => { + manager.onDidCreate(() => { createdCounter++; }); diff --git a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts index af2853ce8b6..dbb629c311f 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts @@ -508,7 +508,7 @@ suite('EditorPane', () => { assert.strictEqual(group.activeEditorPane?.getId(), WorkspaceTrustRequiredEditor.ID); const getEditorPaneIdAsync = () => new Promise(resolve => { - disposables.add(editorService.onDidActiveEditorChange(event => { + disposables.add(editorService.onDidActiveEditorChange(() => { resolve(group.activeEditorPane?.getId()); })); }); diff --git a/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts index 9a19001b453..d2c732e94db 100644 --- a/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts @@ -10,7 +10,7 @@ import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResource import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; -import { ModesRegistry, PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { DisposableStore } from 'vs/base/common/lifecycle'; suite('TextResourceEditorInput', () => { @@ -42,7 +42,7 @@ suite('TextResourceEditorInput', () => { }); test('preferred language (via ctor)', async () => { - ModesRegistry.registerLanguage({ + const registration = accessor.languageService.registerLanguage({ id: 'resource-input-test', }); @@ -60,10 +60,11 @@ suite('TextResourceEditorInput', () => { await input.resolve(); assert.strictEqual(model.textEditorModel?.getLanguageId(), PLAINTEXT_LANGUAGE_ID); + registration.dispose(); }); test('preferred language (via setPreferredLanguageId)', async () => { - ModesRegistry.registerLanguage({ + const registration = accessor.languageService.registerLanguage({ id: 'resource-input-test', }); @@ -76,6 +77,7 @@ suite('TextResourceEditorInput', () => { const model = await input.resolve(); assert.ok(model); assert.strictEqual(model.textEditorModel?.getLanguageId(), 'resource-input-test'); + registration.dispose(); }); test('preferred contents (via ctor)', async () => { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 04ad7017ca3..fbd98039411 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -71,7 +71,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import product from 'vs/platform/product/common/product'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkingCopyService, WorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IWorkingCopyBackupMeta, IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopy, IWorkingCopyBackupMeta, IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { IFilesConfigurationService, FilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; @@ -209,8 +209,17 @@ export interface ITestInstantiationService extends IInstantiationService { stub<T>(service: ServiceIdentifier<T>, ctor: any): T; } +export class TestWorkingCopyService extends WorkingCopyService { + override unregisterWorkingCopy(workingCopy: IWorkingCopy): void { + return super.unregisterWorkingCopy(workingCopy); + } +} + export function workbenchInstantiationService( overrides?: { + environmentService?: (instantiationService: IInstantiationService) => IEnvironmentService; + fileService?: (instantiationService: IInstantiationService) => IFileService; + configurationService?: (instantiationService: IInstantiationService) => TestConfigurationService; textFileService?: (instantiationService: IInstantiationService) => ITextFileService; pathService?: (instantiationService: IInstantiationService) => IPathService; editorService?: (instantiationService: IInstantiationService) => IEditorService; @@ -218,19 +227,20 @@ export function workbenchInstantiationService( textEditorService?: (instantiationService: IInstantiationService) => ITextEditorService; }, disposables: DisposableStore = new DisposableStore() -): ITestInstantiationService { +): TestInstantiationService { const instantiationService = new TestInstantiationService(new ServiceCollection([ILifecycleService, new TestLifecycleService()])); instantiationService.stub(IEditorWorkerService, new TestEditorWorkerService()); - instantiationService.stub(IWorkingCopyService, disposables.add(new WorkingCopyService())); - instantiationService.stub(IEnvironmentService, TestEnvironmentService); - instantiationService.stub(IWorkbenchEnvironmentService, TestEnvironmentService); + instantiationService.stub(IWorkingCopyService, disposables.add(new TestWorkingCopyService())); + const environmentService = overrides?.environmentService ? overrides.environmentService(instantiationService) : TestEnvironmentService; + instantiationService.stub(IEnvironmentService, environmentService); + instantiationService.stub(IWorkbenchEnvironmentService, environmentService); const contextKeyService = overrides?.contextKeyService ? overrides.contextKeyService(instantiationService) : instantiationService.createInstance(MockContextKeyService); instantiationService.stub(IContextKeyService, contextKeyService); instantiationService.stub(IProgressService, new TestProgressService()); const workspaceContextService = new TestContextService(TestWorkspace); instantiationService.stub(IWorkspaceContextService, workspaceContextService); - const configService = new TestConfigurationService({ + const configService = overrides?.configurationService ? overrides.configurationService(instantiationService) : new TestConfigurationService({ files: { participants: { timeout: 60000 @@ -259,7 +269,7 @@ export function workbenchInstantiationService( instantiationService.stub(IThemeService, themeService); instantiationService.stub(ILanguageConfigurationService, new TestLanguageConfigurationService()); instantiationService.stub(IModelService, disposables.add(instantiationService.createInstance(ModelService))); - const fileService = new TestFileService(); + const fileService = overrides?.fileService ? overrides.fileService(instantiationService) : new TestFileService(); instantiationService.stub(IFileService, fileService); instantiationService.stub(IUriIdentityService, new UriIdentityService(fileService)); instantiationService.stub(IWorkingCopyBackupService, new TestWorkingCopyBackupService()); @@ -309,7 +319,7 @@ export class TestServiceAccessor { @IFileService public fileService: TestFileService, @IFileDialogService public fileDialogService: TestFileDialogService, @IDialogService public dialogService: TestDialogService, - @IWorkingCopyService public workingCopyService: IWorkingCopyService, + @IWorkingCopyService public workingCopyService: TestWorkingCopyService, @IEditorService public editorService: TestEditorService, @IWorkbenchEnvironmentService public environmentService: IWorkbenchEnvironmentService, @IPathService public pathService: IPathService, @@ -854,7 +864,7 @@ export class TestEditorGroupView implements IEditorGroupView { copyEditor(_editor: EditorInput, _target: IEditorGroup, _options?: IEditorOptions): void { } copyEditors(_editors: EditorInputWithOptions[], _target: IEditorGroup): void { } async closeEditor(_editor?: EditorInput, options?: ICloseEditorOptions): Promise<boolean> { return true; } - async closeEditors(_editors: EditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise<void> { } + async closeEditors(_editors: EditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise<boolean> { return true; } async closeAllEditors(options?: ICloseAllEditorsOptions): Promise<void> { } async replaceEditors(_editors: IEditorReplacement[]): Promise<void> { } pinEditor(_editor?: EditorInput): void { } diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index 2150dd3f14b..87f933f5fe0 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -81,14 +81,12 @@ import 'vs/platform/telemetry/electron-sandbox/customEndpointTelemetryService'; import 'vs/workbench/services/files/electron-sandbox/elevatedFileService'; import 'vs/workbench/services/search/electron-sandbox/searchService'; import 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService'; +import 'vs/workbench/services/userDataSync/browser/userDataSyncEnablementService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; -import { IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; -import { UserDataSyncEnablementService } from 'vs/workbench/services/userDataSync/browser/userDataSyncEnablementService'; registerSingleton(IUserDataInitializationService, UserDataInitializationService); -registerSingleton(IUserDataSyncEnablementService, UserDataSyncEnablementService); //#endregion diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index cb8ee5c4ce7..8f3d9020fd0 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -60,6 +60,7 @@ import 'vs/workbench/services/workingCopy/browser/workingCopyBackupService'; 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 { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; @@ -72,7 +73,7 @@ import { ExtensionManagementService } from 'vs/workbench/services/extensionManag import { ILoggerService, LogLevel } from 'vs/platform/log/common/log'; import { FileLoggerService } from 'vs/platform/log/common/fileLog'; import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; -import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataAutoSyncService, IUserDataSyncBackupStoreService, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataAutoSyncService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; @@ -87,9 +88,7 @@ import { ITimerService, TimerService } from 'vs/workbench/services/timer/browser 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'; -import { WebUserDataSyncEnablementService } from 'vs/workbench/services/userDataSync/browser/userDataSyncEnablementService'; -registerSingleton(IUserDataSyncEnablementService, WebUserDataSyncEnablementService); registerSingleton(IWorkbenchExtensionManagementService, ExtensionManagementService); registerSingleton(IAccessibilityService, AccessibilityService, true); registerSingleton(IContextMenuService, ContextMenuService); diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index bf25f5ac2e5..34c0b1e1a50 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -3249,7 +3249,7 @@ declare module 'vscode' { /** * Include the declaration of the current symbol. */ - includeDeclaration: boolean; + readonly includeDeclaration: boolean; } /** @@ -11412,6 +11412,9 @@ declare module 'vscode' { * otherwise will be watched non-recursively (i.e. only changes to the first level of the path * will be reported). * + * If possible, keep the use of recursive watchers to a minimum because recursive file watching + * is quite resource intense. + * * Providing a `string` as `globPattern` acts as convenience method for watching file events in * all opened workspace folders. It cannot be used to add more folders for file watching, nor will * it report any file events from folders that are not part of the opened workspace folders. @@ -11420,10 +11423,11 @@ declare module 'vscode' { * * To stop listening to events the watcher must be disposed. * - * *Note* that file events from file watchers may be excluded based on user configuration. + * *Note* that file events from recursive file watchers may be excluded based on user configuration. * The setting `files.watcherExclude` helps to reduce the overhead of file events from folders * that are known to produce many file changes at once (such as `node_modules` folders). As such, - * it is highly recommended to watch with simple patterns that do not require recursive watchers. + * it is highly recommended to watch with simple patterns that do not require recursive watchers + * where the exclude settings are ignored and you have full control over the events. * * *Note* that symbolic links are not automatically followed for file watching unless the path to * watch itself is a symbolic link. @@ -13029,7 +13033,7 @@ declare module 'vscode' { * _Note_ that controller selection is persisted (by the controllers {@link NotebookController.id id}) and restored as soon as a * controller is re-created or as a notebook is {@link workspace.onDidOpenNotebookDocument opened}. */ - readonly onDidChangeSelectedNotebooks: Event<{ notebook: NotebookDocument; selected: boolean }>; + readonly onDidChangeSelectedNotebooks: Event<{ readonly notebook: NotebookDocument; readonly selected: boolean }>; /** * A controller can set affinities for specific notebook documents. This allows a controller diff --git a/src/vscode-dts/vscode.proposed.notebookConcatTextDocument.d.ts b/src/vscode-dts/vscode.proposed.notebookConcatTextDocument.d.ts index fd4e0e0b001..1350d276088 100644 --- a/src/vscode-dts/vscode.proposed.notebookConcatTextDocument.d.ts +++ b/src/vscode-dts/vscode.proposed.notebookConcatTextDocument.d.ts @@ -9,11 +9,7 @@ declare module 'vscode' { export namespace notebooks { /** - * Create a document that is the concatenation of all notebook cells. By default all code-cells are included - * but a selector can be provided to narrow to down the set of cells. - * - * @param notebook - * @param selector + * @deprecated */ // todo@API really needed? we didn't find a user here export function createConcatTextDocument(notebook: NotebookDocument, selector?: DocumentSelector): NotebookConcatTextDocument; diff --git a/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts b/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts index c9ac1073772..85c7f21a5ff 100644 --- a/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts +++ b/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts @@ -8,6 +8,9 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/106744 export interface NotebookCellOutput { + /** + * @deprecated + */ id: string; } } diff --git a/src/vscode-dts/vscode.proposed.notebookEditor.d.ts b/src/vscode-dts/vscode.proposed.notebookEditor.d.ts index 142e61a6a11..824d3ed5fee 100644 --- a/src/vscode-dts/vscode.proposed.notebookEditor.d.ts +++ b/src/vscode-dts/vscode.proposed.notebookEditor.d.ts @@ -69,58 +69,6 @@ declare module 'vscode' { readonly viewColumn?: ViewColumn; } - /** @deprecated */ - export interface NotebookDocumentMetadataChangeEvent { - /** - * The {@link NotebookDocument notebook document} for which the document metadata have changed. - */ - //todo@API rename to notebook? - readonly document: NotebookDocument; - } - - /** @deprecated */ - export interface NotebookCellsChangeData { - readonly start: number; - // todo@API end? Use NotebookCellRange instead? - readonly deletedCount: number; - // todo@API removedCells, deletedCells? - readonly deletedItems: NotebookCell[]; - // todo@API addedCells, insertedCells, newCells? - readonly items: NotebookCell[]; - } - - /** @deprecated */ - export interface NotebookCellsChangeEvent { - /** - * The {@link NotebookDocument notebook document} for which the cells have changed. - */ - //todo@API rename to notebook? - readonly document: NotebookDocument; - readonly changes: ReadonlyArray<NotebookCellsChangeData>; - } - - /** @deprecated */ - export interface NotebookCellOutputsChangeEvent { - /** - * The {@link NotebookDocument notebook document} for which the cell outputs have changed. - */ - //todo@API remove? use cell.notebook instead? - readonly document: NotebookDocument; - // NotebookCellOutputsChangeEvent.cells vs NotebookCellMetadataChangeEvent.cell - readonly cells: NotebookCell[]; - } - - /** @deprecated */ - export interface NotebookCellMetadataChangeEvent { - /** - * The {@link NotebookDocument notebook document} for which the cell metadata have changed. - */ - //todo@API remove? use cell.notebook instead? - readonly document: NotebookDocument; - // NotebookCellOutputsChangeEvent.cells vs NotebookCellMetadataChangeEvent.cell - readonly cell: NotebookCell; - } - export interface NotebookEditorSelectionChangeEvent { /** * The {@link NotebookEditor notebook editor} for which the selections have changed. @@ -137,7 +85,6 @@ declare module 'vscode' { readonly visibleRanges: ReadonlyArray<NotebookRange>; } - export interface NotebookDocumentShowOptions { viewColumn?: ViewColumn; preserveFocus?: boolean; @@ -145,21 +92,6 @@ declare module 'vscode' { selections?: NotebookRange[]; } - export namespace notebooks { - /** @deprecated */ - export const onDidSaveNotebookDocument: Event<NotebookDocument>; - /** @deprecated */ - export const onDidChangeNotebookDocumentMetadata: Event<NotebookDocumentMetadataChangeEvent>; - /** @deprecated */ - export const onDidChangeNotebookCells: Event<NotebookCellsChangeEvent>; - // todo@API add onDidChangeNotebookCellOutputs - /** @deprecated */ - export const onDidChangeCellOutputs: Event<NotebookCellOutputsChangeEvent>; - // todo@API add onDidChangeNotebookCellMetadata - /** @deprecated */ - export const onDidChangeCellMetadata: Event<NotebookCellMetadataChangeEvent>; - } - export namespace window { export const visibleNotebookEditors: NotebookEditor[]; export const onDidChangeVisibleNotebookEditors: Event<NotebookEditor[]>; diff --git a/src/vscode-dts/vscode.proposed.tabs.d.ts b/src/vscode-dts/vscode.proposed.tabs.d.ts index 2a1fd5b66d1..0d369caef4f 100644 --- a/src/vscode-dts/vscode.proposed.tabs.d.ts +++ b/src/vscode-dts/vscode.proposed.tabs.d.ts @@ -53,6 +53,7 @@ declare module 'vscode' { * Represents a tab within the window */ export interface Tab { + /** * The text displayed on the tab */ @@ -70,8 +71,8 @@ declare module 'vscode' { readonly kind: TabKindText | TabKindTextDiff | TabKindCustom | TabKindWebview | TabKindNotebook | TabKindNotebookDiff | TabKindTerminal | unknown; /** - * Whether or not the tab is currently active - * Dictated by being the selected tab in the group + * Whether or not the tab is currently active. + * This is dictated by being the selected tab in the group */ readonly isActive: boolean; @@ -131,25 +132,18 @@ declare module 'vscode' { /** * The currently active group */ + // TOD@API name: maybe `activeGroup` to align with `groups` (which isn't tabGroups) readonly activeTabGroup: TabGroup; /** - * An {@link Event} which fires when a group changes. - */ - // TODO@API add TabGroup instance - readonly onDidChangeTabGroup: Event<void>; - - /** - * An {@link Event} which fires when a tab changes. + * An {@link Event event} which fires when {@link TabGroup tab groups} has changed. */ - // TODO@API use richer event type? - readonly onDidChangeTab: Event<Tab>; + readonly onDidChangeTabGroups: Event<TabGroup[]>; /** - * An {@link Event} which fires when the active group changes. - * This does not fire when the properties within the group change. + * An {@link Event event} which fires when a {@link Tab tabs} have changed. */ - readonly onDidChangeActiveTabGroup: Event<TabGroup>; + readonly onDidChangeTabs: Event<Tab[]>; /** * Closes the tab. This makes the tab object invalid and the tab @@ -157,9 +151,12 @@ declare module 'vscode' { * Note: In the case of a dirty tab, a confirmation dialog will be shown which may be cancelled. If cancelled the tab is still valid * @param tab The tab to close, must be reference equal to a tab given by the API * @param preserveFocus When `true` focus will remain in its current position. If `false` it will jump to the next tab. + * @returns A promise that resolves true when then tab is closed. Otherwise it will return false. + * If false is returned the tab is still valid. */ - close(tab: Tab[], preserveFocus?: boolean): Thenable<void>; - close(tab: Tab, preserveFocus?: boolean): Thenable<void>; + close(tab: Tab | Tab[], preserveFocus?: boolean): Thenable<boolean>; + // TODO@API support to close "all" + // close(tab: TabGroup | TabGroup[], preserveFocus?: boolean): Thenable<boolean>; /** * Moves a tab to the given index within the column. @@ -172,6 +169,7 @@ declare module 'vscode' { */ // TODO@API support TabGroup in addition to ViewColumn // TODO@API support just index for moving inside current group + // TODO@API move a tag group move(tab: Tab, viewColumn: ViewColumn, index: number, preserveFocus?: boolean): Thenable<void>; } } diff --git a/src/vscode-dts/vscode.proposed.testObserver.d.ts b/src/vscode-dts/vscode.proposed.testObserver.d.ts index 2bdb21d7473..d4465affbf2 100644 --- a/src/vscode-dts/vscode.proposed.testObserver.d.ts +++ b/src/vscode-dts/vscode.proposed.testObserver.d.ts @@ -69,24 +69,6 @@ declare module 'vscode' { } /** - * A test item is an item shown in the "test explorer" view. It encompasses - * both a suite and a test, since they have almost or identical capabilities. - */ - export interface TestItem { - /** - * Marks the test as outdated. This can happen as a result of file changes, - * for example. In "auto run" mode, tests that are outdated will be - * automatically rerun after a short delay. Invoking this on a - * test with children will mark the entire subtree as outdated. - * - * Extensions should generally not override this method. - */ - // todo@api still unsure about this - invalidateResults(): void; - } - - - /** * TestResults can be provided to the editor in {@link tests.publishTestResult}, * or read from it in {@link tests.testResults}. * diff --git a/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts b/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts index 0af1e3157ba..35a712c749c 100644 --- a/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts +++ b/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts @@ -7,49 +7,17 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/142990 - export interface TextEditorDropEvent { - /** - * The {@link TextEditor} the resource was dropped onto. - */ - readonly editor: TextEditor; - - /** - * The position in the file where the drop occurred - */ - readonly position: Position; - - /** - * The {@link DataTransfer data transfer} associated with this drop. - */ - readonly dataTransfer: DataTransfer; - - /** - * Allows to pause the event to delay apply the drop. - * - * *Note:* This function can only be called during event dispatch and not - * in an asynchronous manner: - * - * ```ts - * workspace.onWillDropOnTextEditor(event => { - * // async, will *throw* an error - * setTimeout(() => event.waitUntil(promise)); - * - * // sync, OK - * event.waitUntil(promise); - * }) - * ``` - * - * @param thenable A thenable that delays saving. - */ - waitUntil(thenable: Thenable<any>): void; + export class SnippetTextEdit { + snippet: SnippetString; + range: Range; + constructor(range: Range, snippet: SnippetString); + } - token: CancellationToken; + export interface DocumentOnDropProvider { + provideDocumentOnDropEdits(document: TextDocument, position: Position, dataTransfer: DataTransfer, token: CancellationToken): ProviderResult<SnippetTextEdit>; } - export namespace workspace { - /** - * Event fired when the user drops a resource into a text editor. - */ - export const onWillDropOnTextEditor: Event<TextEditorDropEvent>; + export namespace languages { + export function registerDocumentOnDropProvider(selector: DocumentSelector, provider: DocumentOnDropProvider): Disposable; } } |