Merge from vscode ad407028575a77ea387eb7cc219b323dc017b686

This commit is contained in:
ADS Merger
2020-08-31 12:35:56 -07:00
committed by Anthony Dresser
parent 404260b8a0
commit 4ad73d381c
480 changed files with 14360 additions and 14122 deletions
+4 -1
View File
@@ -23,6 +23,9 @@ export function clearNode(node: HTMLElement): void {
}
}
/**
* @deprecated use `node.remove()` instead
*/
export function removeNode(node: HTMLElement): void {
if (node.parentNode) {
node.parentNode.removeChild(node);
@@ -1004,7 +1007,7 @@ export function prepend<T extends Node>(parent: HTMLElement, child: T): T {
return child;
}
const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((.([\w\-]+))*)/;
const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/;
export enum Namespace {
HTML = 'http://www.w3.org/1999/xhtml',
+24 -16
View File
@@ -17,6 +17,7 @@ import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { renderCodicons, markdownEscapeEscapedCodicons } from 'vs/base/common/codicons';
import { resolvePath } from 'vs/base/common/resources';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
export interface MarkedOptions extends marked.MarkedOptions {
baseUrl?: never;
@@ -185,25 +186,32 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
const actionHandler = options.actionHandler;
if (actionHandler) {
actionHandler.disposeables.add(DOM.addStandardDisposableListener(element, 'click', event => {
let target: HTMLElement | null = event.target;
if (target.tagName !== 'A') {
target = target.parentElement;
if (!target || target.tagName !== 'A') {
[DOM.EventType.CLICK, DOM.EventType.AUXCLICK].forEach(event => {
actionHandler.disposeables.add(DOM.addDisposableListener(element, event, (e: MouseEvent) => {
const mouseEvent = new StandardMouseEvent(e);
if (!mouseEvent.leftButton && !mouseEvent.middleButton) {
return;
}
}
try {
const href = target.dataset['href'];
if (href) {
actionHandler.callback(href, event);
let target: HTMLElement | null = mouseEvent.target;
if (target.tagName !== 'A') {
target = target.parentElement;
if (!target || target.tagName !== 'A') {
return;
}
}
} catch (err) {
onUnexpectedError(err);
} finally {
event.preventDefault();
}
}));
try {
const href = target.dataset['href'];
if (href) {
actionHandler.callback(href, mouseEvent);
}
} catch (err) {
onUnexpectedError(err);
} finally {
mouseEvent.preventDefault();
}
}));
});
}
// Use our own sanitizer so that we can let through only spans.
@@ -285,6 +285,10 @@ export class ActionBar extends Disposable implements IActionRunner {
index++;
}
});
if (this.focusedItem) {
// After a clear actions might be re-added to simply toggle some actions. We should preserve focus #97128
this.focus(this.focusedItem);
}
}
getWidth(index: number): number {
@@ -187,14 +187,14 @@ class Label {
if (typeof label === 'string') {
if (!this.singleLabel) {
this.container.innerHTML = '';
this.container.innerText = '';
dom.removeClass(this.container, 'multiple');
this.singleLabel = dom.append(this.container, dom.$('a.label-name', { id: options?.domId }));
}
this.singleLabel.textContent = label;
} else {
this.container.innerHTML = '';
this.container.innerText = '';
dom.addClass(this.container, 'multiple');
this.singleLabel = undefined;
@@ -250,7 +250,7 @@ class LabelWithHighlights {
if (typeof label === 'string') {
if (!this.singleLabel) {
this.container.innerHTML = '';
this.container.innerText = '';
dom.removeClass(this.container, 'multiple');
this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), this.supportCodicons);
}
@@ -258,7 +258,7 @@ class LabelWithHighlights {
this.singleLabel.set(label, options?.matches, options?.title, options?.labelEscapeNewLines);
} else {
this.container.innerHTML = '';
this.container.innerText = '';
dom.addClass(this.container, 'multiple');
this.singleLabel = undefined;
+2 -2
View File
@@ -182,7 +182,7 @@ export class InputBox extends Widget {
this.maxHeight = typeof this.options.flexibleMaxHeight === 'number' ? this.options.flexibleMaxHeight : Number.POSITIVE_INFINITY;
this.mirror = dom.append(wrapper, $('div.mirror'));
this.mirror.innerHTML = '&#160;';
this.mirror.innerText = '\u00a0';
this.scrollableElement = new ScrollableElement(this.element, { vertical: ScrollbarVisibility.Auto });
@@ -563,7 +563,7 @@ export class InputBox extends Widget {
if (mirrorTextContent) {
this.mirror.textContent = value + suffix;
} else {
this.mirror.innerHTML = '&#160;';
this.mirror.innerText = '\u00a0';
}
this.layout();
+4
View File
@@ -602,6 +602,10 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
return this.items[index].element;
}
indexOf(element: T): number {
return this.items.findIndex(item => item.element === element);
}
domElement(index: number): HTMLElement | null {
const row = this.items[index].row;
return row && row.domNode;
@@ -1323,6 +1323,10 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
return this.view.element(index);
}
indexOf(element: T): number {
return this.view.indexOf(element);
}
get length(): number {
return this.view.length;
}
+8 -6
View File
@@ -324,8 +324,7 @@ export class Menu extends ActionBar {
if (action instanceof Separator) {
return new MenuSeparatorActionViewItem(options.context, action, { icon: true });
} else if (action instanceof SubmenuAction) {
const actions = Array.isArray(action.actions) ? action.actions : action.actions();
const menuActionViewItem = new SubmenuMenuActionViewItem(action, actions, parentData, { ...options, submenuIds: new Set([...(options.submenuIds || []), action.id]) });
const menuActionViewItem = new SubmenuMenuActionViewItem(action, action.actions, parentData, { ...options, submenuIds: new Set([...(options.submenuIds || []), action.id]) });
if (options.enableMnemonics) {
const mnemonic = menuActionViewItem.getMnemonic();
@@ -791,7 +790,12 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
private cleanupExistingSubmenu(force: boolean): void {
if (this.parentData.submenu && (force || (this.parentData.submenu !== this.mysubmenu))) {
this.parentData.submenu.dispose();
// disposal may throw if the submenu has already been removed
try {
this.parentData.submenu.dispose();
} catch { }
this.parentData.submenu = undefined;
this.updateAriaExpanded('false');
if (this.submenuContainer) {
@@ -835,7 +839,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
if (!this.parentData.submenu) {
this.updateAriaExpanded('true');
this.submenuContainer = document.createElement('div.monaco-submenu');
this.submenuContainer = append(this.element, $('div.monaco-submenu'));
addClasses(this.submenuContainer, 'menubar-menu-items-holder', 'context-view');
// Set the top value of the menu container before construction
@@ -853,8 +857,6 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
this.parentData.submenu.style(this.menuStyle);
}
this.element.appendChild(this.submenuContainer);
// layout submenu
const entryBox = this.element.getBoundingClientRect();
const entryBoxUpdated = {
@@ -217,8 +217,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
// Intercept keyboard handling
// React on KEY_UP since the actionBar also reacts on KEY_UP so that appropriate events get canceled
this._register(dom.addDisposableListener(this.selectElement, dom.EventType.KEY_UP, (e: KeyboardEvent) => {
this._register(dom.addDisposableListener(this.selectElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
const event = new StandardKeyboardEvent(e);
let showDropDown = false;
@@ -235,7 +234,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
if (showDropDown) {
this.showSelectDropDown();
dom.EventHelper.stop(e, true);
dom.EventHelper.stop(e);
}
}));
}
+10 -9
View File
@@ -26,6 +26,7 @@ export interface IToolBarOptions {
actionRunner?: IActionRunner;
toggleMenuTitle?: string;
anchorAlignmentProvider?: () => AnchorAlignment;
renderDropdownAsChildElement?: boolean;
}
/**
@@ -39,6 +40,7 @@ export class ToolBar extends Disposable {
private submenuActionViewItems: DropdownMenuActionViewItem[] = [];
private hasSecondaryActions: boolean = false;
private lookupKeybindings: boolean;
private element: HTMLElement;
private _onDidChangeDropdownVisibility = this._register(new EventMultiplexer<boolean>());
readonly onDidChangeDropdownVisibility = this._onDidChangeDropdownVisibility.event;
@@ -52,11 +54,11 @@ export class ToolBar extends Disposable {
this.toggleMenuAction = this._register(new ToggleMenuAction(() => this.toggleMenuActionViewItem?.show(), options.toggleMenuTitle));
let element = document.createElement('div');
element.className = 'monaco-toolbar';
container.appendChild(element);
this.element = document.createElement('div');
this.element.className = 'monaco-toolbar';
container.appendChild(this.element);
this.actionBar = this._register(new ActionBar(element, {
this.actionBar = this._register(new ActionBar(this.element, {
orientation: options.orientation,
ariaLabel: options.ariaLabel,
actionRunner: options.actionRunner,
@@ -72,7 +74,7 @@ export class ToolBar extends Disposable {
keybindingProvider: this.options.getKeyBinding,
classNames: toolBarMoreIcon.classNames,
anchorAlignmentProvider: this.options.anchorAlignmentProvider,
menuAsChild: true
menuAsChild: !!this.options.renderDropdownAsChildElement
}
);
this.toggleMenuActionViewItem.setActionContext(this.actionBar.context);
@@ -90,10 +92,9 @@ export class ToolBar extends Disposable {
}
if (action instanceof SubmenuAction) {
const actions = Array.isArray(action.actions) ? action.actions : action.actions();
const result = new DropdownMenuActionViewItem(
action,
actions,
action.actions,
contextMenuProvider,
{
actionViewItemProvider: this.options.actionViewItemProvider,
@@ -134,8 +135,8 @@ export class ToolBar extends Disposable {
}
}
getContainer(): HTMLElement {
return this.actionBar.getContainer();
getElement(): HTMLElement {
return this.element;
}
getItemsWidth(): number {
+1 -1
View File
@@ -881,7 +881,7 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
this.messageDomNode.textContent = localize('empty', "No elements found");
this._empty = true;
} else {
this.messageDomNode.innerHTML = '';
this.messageDomNode.innerText = '';
this._empty = false;
}
+6 -1
View File
@@ -256,7 +256,12 @@ export class Separator extends Action {
export type SubmenuActions = IAction[] | (() => IAction[]);
export class SubmenuAction extends Action {
constructor(id: string, label: string, readonly actions: SubmenuActions, cssClass?: string) {
get actions(): IAction[] {
return Array.isArray(this._actions) ? this._actions : this._actions();
}
constructor(id: string, label: string, private _actions: SubmenuActions, cssClass?: string) {
super(id, label, cssClass, true);
}
}
+3
View File
@@ -590,6 +590,9 @@ export function asArray<T>(x: T | T[]): T[] {
return Array.isArray(x) ? x : [x];
}
/**
* @deprecated Use `Array.from` or `[...iter]`
*/
export function toArray<T>(iterable: IterableIterator<T>): T[] {
const result: T[] = [];
for (let element of iterable) {
+24 -44
View File
@@ -33,8 +33,7 @@ const intlFileNameCollatorNumericCaseInsenstive: IdleValue<{ collator: Intl.Coll
return {
collator: collator
};
});
});/** Compares filenames without distinguishing the name from the extension. Disambiguates by unicode comparison. */
export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
const a = one || '';
const b = other || '';
@@ -49,36 +48,16 @@ export function compareFileNames(one: string | null, other: string | null, caseS
return result;
}
/** Compares filenames by name then extension, sorting numbers numerically instead of alphabetically. */
export function compareFileNamesNumeric(one: string | null, other: string | null): number {
const [oneName, oneExtension] = extractNameAndExtension(one, true);
const [otherName, otherExtension] = extractNameAndExtension(other, true);
/** Compares filenames without distinguishing the name from the extension. Disambiguates by length, not unicode comparison. */
export function compareFileNamesDefault(one: string | null, other: string | null): number {
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator;
let result;
one = one || '';
other = other || '';
// Check for name differences, comparing numbers numerically instead of alphabetically.
result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName);
if (result !== 0) {
return result;
}
// Check for case insensitive extension differences, comparing numbers numerically instead of alphabetically.
result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension);
if (result !== 0) {
return result;
}
// Disambiguate the extension case if needed.
if (oneExtension !== otherExtension) {
return collatorNumeric.compare(oneExtension, otherExtension);
}
return 0;
// Compare the entire filename - both name and extension - and disambiguate by length if needed
return compareAndDisambiguateByLength(collatorNumeric, one, other);
}
const FileNameMatch = /^(.*?)(\.([^.]*))?$/;
export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
if (!caseSensitive) {
one = one && one.toLowerCase();
@@ -123,10 +102,12 @@ export function compareFileExtensions(one: string | null, other: string | null):
return result;
}
/** Compares filenames by extenson, then by name. Sorts numbers numerically, not alphabetically. */
export function compareFileExtensionsNumeric(one: string | null, other: string | null): number {
const [oneName, oneExtension] = extractNameAndExtension(one, true);
const [otherName, otherExtension] = extractNameAndExtension(other, true);
/** Compares filenames by extenson, then by full filename */
export function compareFileExtensionsDefault(one: string | null, other: string | null): number {
one = one || '';
other = other || '';
const oneExtension = extractExtension(one);
const otherExtension = extractExtension(other);
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator;
let result;
@@ -137,20 +118,12 @@ export function compareFileExtensionsNumeric(one: string | null, other: string |
return result;
}
// Compare names.
result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName);
if (result !== 0) {
return result;
}
// Disambiguate extension case if needed.
if (oneExtension !== otherExtension) {
return collatorNumeric.compare(oneExtension, otherExtension);
}
return 0;
// Compare full filenames
return compareAndDisambiguateByLength(collatorNumeric, one, other);
}
const FileNameMatch = /^(.*?)(\.([^.]*))?$/;
/** Extracts the name and extension from a full filename, with optional special handling for dotfiles */
function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): [string, string] {
const match = str ? FileNameMatch.exec(str) as Array<string> : ([] as Array<string>);
@@ -166,6 +139,13 @@ function extractNameAndExtension(str?: string | null, dotfilesAsNames = false):
return result;
}
/** Extracts the extension from a full filename. Treats dotfiles as names, not extensions. */
function extractExtension(str?: string | null): string {
const match = str ? FileNameMatch.exec(str) as Array<string> : ([] as Array<string>);
return (match && match[1] && match[1].charAt(0) !== '.' && match[3]) || '';
}
function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, other: string) {
// Check for differences
let result = collator.compare(one, other);
+28 -42
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { compareAnything } from 'vs/base/common/comparers';
import { matchesPrefix, IMatch, matchesCamelCase, isUpper, fuzzyScore, createMatches as createFuzzyMatches, matchesStrictPrefix } from 'vs/base/common/filters';
import { matchesPrefix, IMatch, isUpper, fuzzyScore, createMatches as createFuzzyMatches, matchesStrictPrefix } from 'vs/base/common/filters';
import { sep } from 'vs/base/common/path';
import { isWindows, isLinux } from 'vs/base/common/platform';
import { stripWildcards, equalsIgnoreCase } from 'vs/base/common/strings';
@@ -168,7 +168,7 @@ function computeCharScore(queryCharAtIndex: string, queryLowerCharAtIndex: strin
score += 1;
// if (DEBUG) {
// console.groupCollapsed(`%cCharacter match bonus: +1 (char: ${queryLower[queryIndex]} at index ${targetIndex}, total score: ${score})`, 'font-weight: normal');
// console.groupCollapsed(`%cCharacter match bonus: +1 (char: ${queryLowerCharAtIndex} at index ${targetIndex}, total score: ${score})`, 'font-weight: normal');
// }
// Consecutive match bonus
@@ -176,7 +176,7 @@ function computeCharScore(queryCharAtIndex: string, queryLowerCharAtIndex: strin
score += (matchesSequenceLength * 5);
// if (DEBUG) {
// console.log('Consecutive match bonus: ' + (matchesSequenceLength * 5));
// console.log(`Consecutive match bonus: +${matchesSequenceLength * 5}`);
// }
}
@@ -206,16 +206,16 @@ function computeCharScore(queryCharAtIndex: string, queryLowerCharAtIndex: strin
score += separatorBonus;
// if (DEBUG) {
// console.log('After separtor bonus: +4');
// console.log(`After separtor bonus: +${separatorBonus}`);
// }
}
// Inside word upper case bonus (camel case)
else if (isUpper(target.charCodeAt(targetIndex))) {
score += 1;
score += 2;
// if (DEBUG) {
// console.log('Inside word upper case bonus: +1');
// console.log('Inside word upper case bonus: +2');
// }
}
}
@@ -371,8 +371,7 @@ export interface IItemAccessor<T> {
const PATH_IDENTITY_SCORE = 1 << 18;
const LABEL_PREFIX_SCORE_MATCHCASE = 1 << 17;
const LABEL_PREFIX_SCORE_IGNORECASE = 1 << 16;
const LABEL_CAMELCASE_SCORE = 1 << 15;
const LABEL_SCORE_THRESHOLD = 1 << 14;
const LABEL_SCORE_THRESHOLD = 1 << 15;
export function scoreItemFuzzy<T>(item: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor<T>, cache: FuzzyScorerCache): IItemScore {
if (!item || !query.normalized) {
@@ -386,11 +385,17 @@ export function scoreItemFuzzy<T>(item: T, query: IPreparedQuery, fuzzy: boolean
const description = accessor.getItemDescription(item);
// in order to speed up scoring, we cache the score with a unique hash based on:
// - label
// - description (if provided)
// - query (normalized)
// - number of query pieces (i.e. 'hello world' and 'helloworld' are different)
// - wether fuzzy matching is enabled or not
let cacheHash: string;
if (description) {
cacheHash = `${label}${description}${query.normalized}${fuzzy}`;
cacheHash = `${label}${description}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}`;
} else {
cacheHash = `${label}${query.normalized}${fuzzy}`;
cacheHash = `${label}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}`;
}
const cached = cache[cacheHash];
@@ -465,13 +470,7 @@ function doScoreItemFuzzySingle(label: string, description: string | undefined,
return { score: prefixLabelMatchStrictCase ? LABEL_PREFIX_SCORE_MATCHCASE : LABEL_PREFIX_SCORE_IGNORECASE, labelMatch: prefixLabelMatchStrictCase || prefixLabelMatchIgnoreCase };
}
// Treat camelcase matches on the label second highest
const camelcaseLabelMatch = matchesCamelCase(query.normalized, label);
if (camelcaseLabelMatch) {
return { score: LABEL_CAMELCASE_SCORE, labelMatch: camelcaseLabelMatch };
}
// Prefer scores on the label if any
// Second, score fuzzy
const [labelScore, labelPositions] = scoreFuzzy(label, query.normalized, query.normalizedLowercase, fuzzy);
if (labelScore) {
return { score: labelScore + LABEL_SCORE_THRESHOLD, labelMatch: createMatches(labelPositions) };
@@ -594,7 +593,7 @@ export function compareItemsByFuzzyScore<T>(itemA: T, itemB: T, query: IPrepared
const scoreA = itemScoreA.score;
const scoreB = itemScoreB.score;
// 1.) prefer identity matches
// 1.) identity matches have highest score
if (scoreA === PATH_IDENTITY_SCORE || scoreB === PATH_IDENTITY_SCORE) {
if (scoreA !== scoreB) {
return scoreA === PATH_IDENTITY_SCORE ? -1 : 1;
@@ -631,44 +630,32 @@ export function compareItemsByFuzzyScore<T>(itemA: T, itemB: T, query: IPrepared
}
}
// 4.) prefer camelcase matches
if (scoreA === LABEL_CAMELCASE_SCORE || scoreB === LABEL_CAMELCASE_SCORE) {
// 4.) matches on label are considered higher compared to label+description matches
if (scoreA > LABEL_SCORE_THRESHOLD || scoreB > LABEL_SCORE_THRESHOLD) {
if (scoreA !== scoreB) {
return scoreA === LABEL_CAMELCASE_SCORE ? -1 : 1;
return scoreA > scoreB ? -1 : 1;
}
const labelA = accessor.getItemLabel(itemA) || '';
const labelB = accessor.getItemLabel(itemB) || '';
// prefer more compact camel case matches over longer
// prefer more compact matches over longer in label
const comparedByMatchLength = compareByMatchLength(itemScoreA.labelMatch, itemScoreB.labelMatch);
if (comparedByMatchLength !== 0) {
return comparedByMatchLength;
}
// prefer shorter names when both match on label camelcase
// prefer shorter labels over longer labels
const labelA = accessor.getItemLabel(itemA) || '';
const labelB = accessor.getItemLabel(itemB) || '';
if (labelA.length !== labelB.length) {
return labelA.length - labelB.length;
}
}
// 5.) prefer label scores
if (scoreA > LABEL_SCORE_THRESHOLD || scoreB > LABEL_SCORE_THRESHOLD) {
if (scoreB < LABEL_SCORE_THRESHOLD) {
return -1;
}
if (scoreA < LABEL_SCORE_THRESHOLD) {
return 1;
}
}
// 6.) compare by score
// 5.) compare by score in label+description
if (scoreA !== scoreB) {
return scoreA > scoreB ? -1 : 1;
}
// 7.) prefer matches in label over non-label matches
// 6.) scores are identical: prefer matches in label over non-label matches
const itemAHasLabelMatches = Array.isArray(itemScoreA.labelMatch) && itemScoreA.labelMatch.length > 0;
const itemBHasLabelMatches = Array.isArray(itemScoreB.labelMatch) && itemScoreB.labelMatch.length > 0;
if (itemAHasLabelMatches && !itemBHasLabelMatches) {
@@ -677,15 +664,14 @@ export function compareItemsByFuzzyScore<T>(itemA: T, itemB: T, query: IPrepared
return 1;
}
// 8.) scores are identical, prefer more compact matches (label and description)
// 7.) scores are identical: prefer more compact matches (label and description)
const itemAMatchDistance = computeLabelAndDescriptionMatchDistance(itemA, itemScoreA, accessor);
const itemBMatchDistance = computeLabelAndDescriptionMatchDistance(itemB, itemScoreB, accessor);
if (itemAMatchDistance && itemBMatchDistance && itemAMatchDistance !== itemBMatchDistance) {
return itemBMatchDistance > itemAMatchDistance ? -1 : 1;
}
// 9.) at this point, scores are identical and match compactness as well
// for both items so we start to use the fallback compare
// 8.) scores are identical: start to use the fallback compare
return fallbackCompare(itemA, itemB, query, accessor);
}
+11 -6
View File
@@ -8,7 +8,12 @@ import * as strings from 'vs/base/common/strings';
/**
* Return a hash value for an object.
*/
export function hash(obj: any, hashVal = 0): number {
export function hash(obj: any): number {
return doHash(obj, 0);
}
export function doHash(obj: any, hashVal: number): number {
switch (typeof obj) {
case 'object':
if (obj === null) {
@@ -24,9 +29,9 @@ export function hash(obj: any, hashVal = 0): number {
case 'number':
return numberHash(obj, hashVal);
case 'undefined':
return numberHash(0, 937);
return numberHash(937, hashVal);
default:
return numberHash(0, 617);
return numberHash(617, hashVal);
}
}
@@ -48,14 +53,14 @@ export function stringHash(s: string, hashVal: number) {
function arrayHash(arr: any[], initialHashVal: number): number {
initialHashVal = numberHash(104579, initialHashVal);
return arr.reduce((hashVal, item) => hash(item, hashVal), initialHashVal);
return arr.reduce((hashVal, item) => doHash(item, hashVal), initialHashVal);
}
function objectHash(obj: any, initialHashVal: number): number {
initialHashVal = numberHash(181387, initialHashVal);
return Object.keys(obj).sort().reduce((hashVal, key) => {
hashVal = stringHash(key, hashVal);
return hash(obj[key], hashVal);
return doHash(obj[key], hashVal);
}, initialHashVal);
}
@@ -68,7 +73,7 @@ export class Hasher {
}
hash(obj: any): number {
this._value = hash(obj, this._value);
this._value = doHash(obj, this._value);
return this._value;
}
}
+28 -4
View File
@@ -45,6 +45,14 @@ function trackDisposable<T extends IDisposable>(x: T): T {
return x;
}
export class MultiDisposeError extends Error {
constructor(
public readonly errors: any[]
) {
super(`Encounter errors while disposing of store. Errors: [${errors.join(', ')}]`);
}
}
export interface IDisposable {
dispose(): void;
}
@@ -60,12 +68,25 @@ export function dispose<T extends IDisposable>(disposables: Array<T>): Array<T>;
export function dispose<T extends IDisposable>(disposables: ReadonlyArray<T>): ReadonlyArray<T>;
export function dispose<T extends IDisposable>(arg: T | IterableIterator<T> | undefined): any {
if (Iterable.is(arg)) {
for (let d of arg) {
let errors: any[] = [];
for (const d of arg) {
if (d) {
markTracked(d);
d.dispose();
try {
d.dispose();
} catch (e) {
errors.push(e);
}
}
}
if (errors.length === 1) {
throw errors[0];
} else if (errors.length > 1) {
throw new MultiDisposeError(errors);
}
return Array.isArray(arg) ? [] : arg;
} else if (arg) {
markTracked(arg);
@@ -116,8 +137,11 @@ export class DisposableStore implements IDisposable {
* Dispose of all registered disposables but do not mark this object as disposed.
*/
public clear(): void {
this._toDispose.forEach(item => item.dispose());
this._toDispose.clear();
try {
dispose(this._toDispose.values());
} finally {
this._toDispose.clear();
}
}
public add<T extends IDisposable>(t: T): T {
File diff suppressed because it is too large Load Diff
+2
View File
@@ -58,6 +58,8 @@ export namespace Schemas {
export const vscodeNotebook = 'vscode-notebook';
export const vscodeNotebookCell = 'vscode-notebook-cell';
export const vscodeSettings = 'vscode-settings';
export const webviewPanel = 'webview-panel';
-4
View File
@@ -855,10 +855,6 @@ export function stripUTF8BOM(str: string): string {
return startsWithUTF8BOM(str) ? str.substr(1) : str;
}
export function safeBtoa(str: string): string {
return btoa(encodeURIComponent(str)); // we use encodeURIComponent because btoa fails for non Latin 1 values
}
/**
* @deprecated ES6
*/
+3 -2
View File
@@ -219,7 +219,8 @@ export function listProcesses(rootPid: number): Promise<ProcessItem> {
// Set numeric locale to ensure '.' is used as the decimal separator
exec(`${ps} ${args}`, { maxBuffer: 1000 * 1024, env: { LC_NUMERIC: 'en_US.UTF-8' } }, (err, stdout, stderr) => {
if (err || stderr) {
// Silently ignoring the screen size is bogus error. See https://github.com/microsoft/vscode/issues/98590
if (err || (stderr && !stderr.includes('screen size is bogus'))) {
reject(err || new Error(stderr.toString()));
} else {
parsePsOutput(stdout, addToTree);
@@ -246,4 +247,4 @@ function parsePsOutput(stdout: string, addToTree: (pid: number, ppid: number, cm
addToTree(parseInt(matches[1]), parseInt(matches[2]), matches[5], parseFloat(matches[3]), parseFloat(matches[4]));
}
}
}
}
@@ -5,13 +5,14 @@
import { Menu, MenuItem, BrowserWindow, ipcMain, IpcMainEvent } from 'electron';
import { ISerializableContextMenuItem, CONTEXT_MENU_CLOSE_CHANNEL, CONTEXT_MENU_CHANNEL, IPopupOptions } from 'vs/base/parts/contextmenu/common/contextmenu';
import { withNullAsUndefined } from 'vs/base/common/types';
export function registerContextMenuListener(): void {
ipcMain.on(CONTEXT_MENU_CHANNEL, (event: IpcMainEvent, contextMenuId: number, items: ISerializableContextMenuItem[], onClickChannel: string, options?: IPopupOptions) => {
const menu = createMenu(event, onClickChannel, items);
menu.popup({
window: BrowserWindow.fromWebContents(event.sender),
window: withNullAsUndefined(BrowserWindow.fromWebContents(event.sender)),
x: options ? options.x : undefined,
y: options ? options.y : undefined,
positioningItem: options ? options.positioningItem : undefined,
@@ -278,7 +278,7 @@ class QuickInput extends Disposable implements IQuickInput {
if (title && this.ui.title.textContent !== title) {
this.ui.title.textContent = title;
} else if (!title && this.ui.title.innerHTML !== '&nbsp;') {
this.ui.title.innerHTML = '&nbsp;';
this.ui.title.innerText = '\u00a0;';
}
const description = this.getDescription();
if (this.ui.description.textContent !== description) {
@@ -381,7 +381,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
private static readonly DEFAULT_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results.");
private _value = '';
private _ariaLabel = QuickPick.DEFAULT_ARIA_LABEL;
private _ariaLabel: string | undefined;
private _placeholder: string | undefined;
private readonly onDidChangeValueEmitter = this._register(new Emitter<string>());
private readonly onDidAcceptEmitter = this._register(new Emitter<IQuickPickAcceptEvent>());
@@ -435,8 +435,8 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
filterValue = (value: string) => value;
set ariaLabel(ariaLabel: string) {
this._ariaLabel = ariaLabel || QuickPick.DEFAULT_ARIA_LABEL;
set ariaLabel(ariaLabel: string | undefined) {
this._ariaLabel = ariaLabel;
this.update();
}
@@ -884,8 +884,11 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
}
if (inputShownJustForScreenReader) {
this.ui.inputBox.ariaLabel = '';
} else if (this.ui.inputBox.ariaLabel !== this.ariaLabel) {
this.ui.inputBox.ariaLabel = this.ariaLabel;
} else {
const ariaLabel = this.ariaLabel || this.placeholder || QuickPick.DEFAULT_ARIA_LABEL;
if (this.ui.inputBox.ariaLabel !== ariaLabel) {
this.ui.inputBox.ariaLabel = ariaLabel;
}
}
this.ui.list.matchOnDescription = this.matchOnDescription;
this.ui.list.matchOnDetail = this.matchOnDetail;
@@ -1384,9 +1387,6 @@ export class QuickInputController extends Disposable {
];
input.canSelectMany = !!options.canPickMany;
input.placeholder = options.placeHolder;
if (options.placeHolder) {
input.ariaLabel = options.placeHolder;
}
input.ignoreFocusOut = !!options.ignoreFocusLost;
input.matchOnDescription = !!options.matchOnDescription;
input.matchOnDetail = !!options.matchOnDetail;
@@ -199,7 +199,7 @@ export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
*/
filterValue: (value: string) => string;
ariaLabel: string;
ariaLabel: string | undefined;
placeholder: string | undefined;
@@ -209,37 +209,6 @@ export interface SaveDialogReturnValue {
bookmark?: string;
}
export interface CrashReporterStartOptions {
companyName: string;
/**
* URL that crash reports will be sent to as POST.
*/
submitURL: string;
/**
* Defaults to `app.name`.
*/
productName?: string;
/**
* Whether crash reports should be sent to the server. Default is `true`.
*/
uploadToServer?: boolean;
/**
* Default is `false`.
*/
ignoreSystemCrashHandler?: boolean;
/**
* An object you can define that will be sent along with the report. Only string
* properties are sent correctly. Nested objects are not supported. When using
* Windows, the property names and values must be fewer than 64 characters.
*/
extra?: Record<string, string>;
/**
* Directory to store the crash reports temporarily (only used when the crash
* reporter is started via `process.crashReporter.start`).
*/
crashesDirectory?: string;
}
export interface FileFilter {
// Docs: http://electronjs.org/docs/api/structures/file-filter
@@ -281,3 +250,62 @@ export interface MouseInputEvent extends InputEvent {
x: number;
y: number;
}
export interface CrashReporterStartOptions {
/**
* URL that crash reports will be sent to as POST.
*/
submitURL: string;
/**
* Defaults to `app.name`.
*/
productName?: string;
/**
* Deprecated alias for `{ globalExtra: { _companyName: ... } }`.
*
* @deprecated
*/
companyName?: string;
/**
* Whether crash reports should be sent to the server. If false, crash reports will
* be collected and stored in the crashes directory, but not uploaded. Default is
* `true`.
*/
uploadToServer?: boolean;
/**
* If true, crashes generated in the main process will not be forwarded to the
* system crash handler. Default is `false`.
*/
ignoreSystemCrashHandler?: boolean;
/**
* If true, limit the number of crashes uploaded to 1/hour. Default is `false`.
*
* @platform darwin,win32
*/
rateLimit?: boolean;
/**
* If true, crash reports will be compressed and uploaded with `Content-Encoding:
* gzip`. Not all collection servers support compressed payloads. Default is
* `false`.
*
* @platform darwin,win32
*/
compress?: boolean;
/**
* Extra string key/value annotations that will be sent along with crash reports
* that are generated in the main process. Only string values are supported.
* Crashes generated in child processes will not contain these extra parameters to
* crash reports generated from child processes, call `addExtraParameter` from the
* child process.
*/
extra?: Record<string, string>;
/**
* Extra string key/value annotations that will be sent along with any crash
* reports generated in any process. These annotations cannot be changed once the
* crash reporter has been started. If a key is present in both the global extra
* parameters and the process-specific extra parameters, then the global one will
* take precedence. By default, `productName` and the app version are included, as
* well as the Electron version.
*/
globalExtra?: Record<string, string>;
}
@@ -74,15 +74,16 @@
},
/**
* Support for subset of methods of Electron's `crashReporter` type.
* Support for subset of methods of Electron's `crashReporter` type.
*/
crashReporter: {
/**
* @param {Electron.CrashReporterStartOptions} options
* @param {string} key
* @param {string} value
*/
start(options) {
crashReporter.start(options);
addExtraParameter(key, value) {
crashReporter.addExtraParameter(key, value);
}
},
@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CrashReporterStartOptions } from 'vs/base/parts/sandbox/common/electronTypes';
export const ipcRenderer = (window as any).vscode.ipcRenderer as {
/**
@@ -54,32 +52,23 @@ export const webFrame = (window as any).vscode.webFrame as {
export const crashReporter = (window as any).vscode.crashReporter as {
/**
* You are required to call this method before using any other `crashReporter` APIs
* and in each process (main/renderer) from which you want to collect crash
* reports. You can pass different options to `crashReporter.start` when calling
* from different processes.
* Set an extra parameter to be sent with the crash report. The values specified
* here will be sent in addition to any values set via the `extra` option when
* `start` was called.
*
* **Note** Child processes created via the `child_process` module will not have
* access to the Electron modules. Therefore, to collect crash reports from them,
* use `process.crashReporter.start` instead. Pass the same options as above along
* with an additional one called `crashesDirectory` that should point to a
* directory to store the crash reports temporarily. You can test this out by
* calling `process.crash()` to crash the child process.
* Parameters added in this fashion (or via the `extra` parameter to
* `crashReporter.start`) are specific to the calling process. Adding extra
* parameters in the main process will not cause those parameters to be sent along
* with crashes from renderer or other child processes. Similarly, adding extra
* parameters in a renderer process will not result in those parameters being sent
* with crashes that occur in other renderer processes or in the main process.
*
* **Note:** If you need send additional/updated `extra` parameters after your
* first call `start` you can call `addExtraParameter` on macOS or call `start`
* again with the new/updated `extra` parameters on Linux and Windows.
*
* **Note:** On macOS and windows, Electron uses a new `crashpad` client for crash
* collection and reporting. If you want to enable crash reporting, initializing
* `crashpad` from the main process using `crashReporter.start` is required
* regardless of which process you want to collect crashes from. Once initialized
* this way, the crashpad handler collects crashes from all processes. You still
* have to call `crashReporter.start` from the renderer or child process, otherwise
* crashes from them will get reported without `companyName`, `productName` or any
* of the `extra` information.
* **Note:** Parameters have limits on the length of the keys and values. Key names
* must be no longer than 39 bytes, and values must be no longer than 127 bytes.
* Keys with names longer than the maximum will be silently ignored. Key values
* longer than the maximum length will be truncated.
*/
start(options: CrashReporterStartOptions): void;
addExtraParameter(key: string, value: string): void;
};
export const process = (window as any).vscode.process as {
+2 -3
View File
@@ -9,7 +9,6 @@ import * as Lifecycle from 'vs/base/common/lifecycle';
import * as DOM from 'vs/base/browser/dom';
import * as Diff from 'vs/base/common/diff/diff';
import * as Touch from 'vs/base/browser/touch';
import * as strings from 'vs/base/common/strings';
import * as Mouse from 'vs/base/browser/mouseEvent';
import * as Keyboard from 'vs/base/browser/keyboardEvent';
import * as Model from 'vs/base/parts/tree/browser/treeModel';
@@ -218,7 +217,7 @@ export class ViewItem implements IViewItem {
this.element.setAttribute('aria-posinset', accessibility.getPosInSet(this.context.tree, this.model.getElement()));
}
if (this.model.hasTrait('focused')) {
const base64Id = strings.safeBtoa(this.model.id);
const base64Id = btoa(encodeURIComponent(this.model.id));
this.element.setAttribute('id', base64Id);
this.element.setAttribute('aria-selected', 'true');
} else {
@@ -1061,7 +1060,7 @@ export class TreeView extends HeightMap {
// ARIA
if (focus) {
this.domNode.setAttribute('aria-activedescendant', strings.safeBtoa(this.context.dataSource.getId(this.context.tree, focus)));
this.domNode.setAttribute('aria-activedescendant', btoa(encodeURIComponent(this.context.dataSource.getId(this.context.tree, focus))));
} else {
this.domNode.removeAttribute('aria-activedescendant');
}
+87 -98
View File
@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { compareFileNames, compareFileExtensions, compareFileNamesNumeric, compareFileExtensionsNumeric } from 'vs/base/common/comparers';
import { compareFileNames, compareFileExtensions, compareFileNamesDefault, compareFileExtensionsDefault } from 'vs/base/common/comparers';
import * as assert from 'assert';
const compareLocale = (a: string, b: string) => a.localeCompare(b);
@@ -15,7 +15,7 @@ suite('Comparers', () => {
test('compareFileNames', () => {
//
// Comparisons with the same results as compareFileNamesNumeric
// Comparisons with the same results as compareFileNamesDefault
//
// name-only comparisons
@@ -28,6 +28,7 @@ suite('Comparers', () => {
// name plus extension comparisons
assert(compareFileNames('bbb.aaa', 'aaa.bbb') > 0, 'files with extensions are compared first by filename');
assert(compareFileNames('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole name all at once by locale');
// dotfile comparisons
assert(compareFileNames('.abc', '.abc') === 0, 'equal dotfile names should be equal');
@@ -52,7 +53,7 @@ suite('Comparers', () => {
assert(compareFileNames('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number');
//
// Comparisons with different results than compareFileNamesNumeric
// Comparisons with different results than compareFileNamesDefault
//
// name-only comparisons
@@ -61,9 +62,6 @@ suite('Comparers', () => {
assert.notDeepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNames), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases do not sort in locale order');
assert.notDeepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNames), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents do not sort in locale order');
// name plus extension comparisons
assert(compareFileNames('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole name all at once by locale');
// numeric comparisons
assert(compareFileNames('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order');
assert(compareFileNames('abc.txt1', 'abc.txt01') > 0, 'same name plus extensions with equal numbers sort in unicode order');
@@ -75,7 +73,7 @@ suite('Comparers', () => {
test('compareFileExtensions', () => {
//
// Comparisons with the same results as compareFileExtensionsNumeric
// Comparisons with the same results as compareFileExtensionsDefault
//
// name-only comparisons
@@ -118,12 +116,8 @@ suite('Comparers', () => {
assert(compareFileExtensions('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared');
assert(compareFileExtensions('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically');
// Same extension comparison that has the same result as compareFileExtensionsNumeric, but a different result than compareFileNames
// This is an edge case caused by compareFileNames comparing the whole name all at once instead of the name and then the extension.
assert(compareFileExtensions('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order');
//
// Comparisons with different results from compareFileExtensionsNumeric
// Comparisons with different results from compareFileExtensionsDefault
//
// name-only comparisions
@@ -135,6 +129,7 @@ suite('Comparers', () => {
// name plus extension comparisons
assert(compareFileExtensions('a.MD', 'a.md') !== compareLocale('MD', 'md'), 'case differences in extensions do not sort by locale');
assert(compareFileExtensions('a.md', 'A.md') !== compareLocale('a', 'A'), 'case differences in names do not sort by locale');
assert(compareFileExtensions('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order');
// dotfile comparisons
assert(compareFileExtensions('.env', '.aaa.env') < 0, 'a dotfile with an extension is treated as a name plus an extension - equal extensions');
@@ -152,145 +147,139 @@ suite('Comparers', () => {
});
test('compareFileNamesNumeric', () => {
test('compareFileNamesDefault', () => {
//
// Comparisons with the same results as compareFileNames
//
// name-only comparisons
assert(compareFileNamesNumeric(null, null) === 0, 'null should be equal');
assert(compareFileNamesNumeric(null, 'abc') < 0, 'null should be come before real values');
assert(compareFileNamesNumeric('', '') === 0, 'empty should be equal');
assert(compareFileNamesNumeric('abc', 'abc') === 0, 'equal names should be equal');
assert(compareFileNamesNumeric('z', 'A') > 0, 'z comes is after A regardless of case');
assert(compareFileNamesNumeric('Z', 'a') > 0, 'Z comes after a regardless of case');
assert(compareFileNamesDefault(null, null) === 0, 'null should be equal');
assert(compareFileNamesDefault(null, 'abc') < 0, 'null should be come before real values');
assert(compareFileNamesDefault('', '') === 0, 'empty should be equal');
assert(compareFileNamesDefault('abc', 'abc') === 0, 'equal names should be equal');
assert(compareFileNamesDefault('z', 'A') > 0, 'z comes is after A regardless of case');
assert(compareFileNamesDefault('Z', 'a') > 0, 'Z comes after a regardless of case');
// name plus extension comparisons
assert(compareFileNamesNumeric('file.ext', 'file.ext') === 0, 'equal full names should be equal');
assert(compareFileNamesNumeric('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
assert(compareFileNamesNumeric('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions');
assert(compareFileNamesNumeric('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently');
assert(compareFileNamesDefault('file.ext', 'file.ext') === 0, 'equal full names should be equal');
assert(compareFileNamesDefault('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
assert(compareFileNamesDefault('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions');
assert(compareFileNamesDefault('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently');
assert(compareFileNamesDefault('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole filename in locale order');
// dotfile comparisons
assert(compareFileNamesNumeric('.abc', '.abc') === 0, 'equal dotfile names should be equal');
assert(compareFileNamesNumeric('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly');
assert(compareFileNamesNumeric('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots');
assert(compareFileNamesNumeric('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first');
assert(compareFileNamesNumeric('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot');
assert(compareFileNamesDefault('.abc', '.abc') === 0, 'equal dotfile names should be equal');
assert(compareFileNamesDefault('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly');
assert(compareFileNamesDefault('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots');
assert(compareFileNamesDefault('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first');
assert(compareFileNamesDefault('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot');
// dotfile vs non-dotfile comparisons
assert(compareFileNamesNumeric(null, '.abc') < 0, 'null should come before dotfiles');
assert(compareFileNamesNumeric('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions');
assert(compareFileNamesNumeric('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions');
assert(compareFileNamesNumeric('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files');
assert(compareFileNamesNumeric('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files');
assert(compareFileNamesDefault(null, '.abc') < 0, 'null should come before dotfiles');
assert(compareFileNamesDefault('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions');
assert(compareFileNamesDefault('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions');
assert(compareFileNamesDefault('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files');
assert(compareFileNamesDefault('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files');
// numeric comparisons
assert(compareFileNamesNumeric('1', '1') === 0, 'numerically equal full names should be equal');
assert(compareFileNamesNumeric('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal');
assert(compareFileNamesNumeric('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order');
assert(compareFileNamesNumeric('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long');
assert(compareFileNamesNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically');
assert(compareFileNamesNumeric('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number');
assert(compareFileNamesDefault('1', '1') === 0, 'numerically equal full names should be equal');
assert(compareFileNamesDefault('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal');
assert(compareFileNamesDefault('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order');
assert(compareFileNamesDefault('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long');
assert(compareFileNamesDefault('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically');
assert(compareFileNamesDefault('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number');
//
// Comparisons with different results than compareFileNames
//
// name-only comparisons
assert(compareFileNamesNumeric('a', 'A') === compareLocale('a', 'A'), 'the same letter sorts by locale');
assert(compareFileNamesNumeric('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter sorts by locale');
assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesNumeric), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order');
assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesNumeric), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents sort in locale order');
// name plus extensions comparisons
assert(compareFileNamesNumeric('aggregate.go', 'aggregate_repo.go') < 0, 'compares the name first, then the extension');
assert(compareFileNamesDefault('a', 'A') === compareLocale('a', 'A'), 'the same letter sorts by locale');
assert(compareFileNamesDefault('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter sorts by locale');
assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesDefault), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order');
assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesDefault), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents sort in locale order');
// numeric comparisons
assert(compareFileNamesNumeric('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first');
assert(compareFileNamesNumeric('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first');
assert(compareFileNamesNumeric('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale');
assert(compareFileNamesDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first');
assert(compareFileNamesDefault('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first');
assert(compareFileNamesDefault('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale');
});
test('compareFileExtensionsNumeric', () => {
test('compareFileExtensionsDefault', () => {
//
// Comparisons with the same result as compareFileExtensions
//
// name-only comparisons
assert(compareFileExtensionsNumeric(null, null) === 0, 'null should be equal');
assert(compareFileExtensionsNumeric(null, 'abc') < 0, 'null should come before real files without extensions');
assert(compareFileExtensionsNumeric('', '') === 0, 'empty should be equal');
assert(compareFileExtensionsNumeric('abc', 'abc') === 0, 'equal names should be equal');
assert(compareFileExtensionsNumeric('z', 'A') > 0, 'z comes after A');
assert(compareFileExtensionsNumeric('Z', 'a') > 0, 'Z comes after a');
assert(compareFileExtensionsDefault(null, null) === 0, 'null should be equal');
assert(compareFileExtensionsDefault(null, 'abc') < 0, 'null should come before real files without extensions');
assert(compareFileExtensionsDefault('', '') === 0, 'empty should be equal');
assert(compareFileExtensionsDefault('abc', 'abc') === 0, 'equal names should be equal');
assert(compareFileExtensionsDefault('z', 'A') > 0, 'z comes after A');
assert(compareFileExtensionsDefault('Z', 'a') > 0, 'Z comes after a');
// name plus extension comparisons
assert(compareFileExtensionsNumeric('file.ext', 'file.ext') === 0, 'equal full filenames should be equal');
assert(compareFileExtensionsNumeric('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
assert(compareFileExtensionsNumeric('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions');
assert(compareFileExtensionsNumeric('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first');
assert(compareFileExtensionsNumeric('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names');
assert(compareFileExtensionsNumeric('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore');
assert(compareFileExtensionsNumeric('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name');
assert(compareFileExtensionsDefault('file.ext', 'file.ext') === 0, 'equal full filenames should be equal');
assert(compareFileExtensionsDefault('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
assert(compareFileExtensionsDefault('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions');
assert(compareFileExtensionsDefault('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first');
assert(compareFileExtensionsDefault('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names');
assert(compareFileExtensionsDefault('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name');
// dotfile comparisons
assert(compareFileExtensionsNumeric('.abc', '.abc') === 0, 'equal dotfiles should be equal');
assert(compareFileExtensionsNumeric('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case');
assert(compareFileExtensionsDefault('.abc', '.abc') === 0, 'equal dotfiles should be equal');
assert(compareFileExtensionsDefault('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case');
// dotfile vs non-dotfile comparisons
assert(compareFileExtensionsNumeric(null, '.abc') < 0, 'null should come before dotfiles');
assert(compareFileExtensionsNumeric('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions');
assert(compareFileExtensionsNumeric('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files');
assert(compareFileExtensionsDefault(null, '.abc') < 0, 'null should come before dotfiles');
assert(compareFileExtensionsDefault('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions');
assert(compareFileExtensionsDefault('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files');
// numeric comparisons
assert(compareFileExtensionsNumeric('1', '1') === 0, 'numerically equal full names should be equal');
assert(compareFileExtensionsNumeric('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal');
assert(compareFileExtensionsNumeric('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order');
assert(compareFileExtensionsNumeric('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order');
assert(compareFileExtensionsNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically');
assert(compareFileExtensionsNumeric('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number');
assert(compareFileExtensionsNumeric('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
assert(compareFileExtensionsNumeric('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal');
assert(compareFileExtensionsNumeric('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
assert(compareFileExtensionsNumeric('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long');
assert(compareFileExtensionsNumeric('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared');
assert(compareFileExtensionsNumeric('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically');
// Same extension comparison that has the same result as compareFileExtensions, but a different result than compareFileNames
// This is an edge case caused by compareFileNames comparing the whole name all at once instead of the name and then the extension.
assert(compareFileExtensionsNumeric('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order');
assert(compareFileExtensionsDefault('1', '1') === 0, 'numerically equal full names should be equal');
assert(compareFileExtensionsDefault('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal');
assert(compareFileExtensionsDefault('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order');
assert(compareFileExtensionsDefault('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order');
assert(compareFileExtensionsDefault('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically');
assert(compareFileExtensionsDefault('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number');
assert(compareFileExtensionsDefault('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
assert(compareFileExtensionsDefault('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal');
assert(compareFileExtensionsDefault('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
assert(compareFileExtensionsDefault('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long');
assert(compareFileExtensionsDefault('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared');
assert(compareFileExtensionsDefault('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically');
//
// Comparisons with different results than compareFileExtensions
//
// name-only comparisons
assert(compareFileExtensionsNumeric('a', 'A') === compareLocale('a', 'A'), 'the same letter of different case sorts by locale');
assert(compareFileExtensionsNumeric('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter of different case sorts by locale');
assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsNumeric), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order');
assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsNumeric), ['email', 'Email', 'émail', 'Émail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents sort in locale order');
assert(compareFileExtensionsDefault('a', 'A') === compareLocale('a', 'A'), 'the same letter of different case sorts by locale');
assert(compareFileExtensionsDefault('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter of different case sorts by locale');
assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsDefault), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order');
assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsDefault), ['email', 'Email', 'émail', 'Émail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents sort in locale order');
// name plus extension comparisons
assert(compareFileExtensionsNumeric('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale');
assert(compareFileExtensionsNumeric('a.md', 'A.md') === compareLocale('a', 'A'), 'case differences in names sort by locale');
assert(compareFileExtensionsDefault('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale');
assert(compareFileExtensionsDefault('a.md', 'A.md') === compareLocale('a', 'A'), 'case differences in names sort by locale');
assert(compareFileExtensionsDefault('aggregate.go', 'aggregate_repo.go') > 0, 'names with the same extension sort in full filename locale order');
// dotfile comparisons
assert(compareFileExtensionsNumeric('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots');
assert(compareFileExtensionsNumeric('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first');
assert(compareFileExtensionsDefault('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots');
assert(compareFileExtensionsDefault('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first');
// dotfile vs non-dotfile comparisons
assert(compareFileExtensionsNumeric('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions');
assert(compareFileExtensionsNumeric('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files');
assert(compareFileExtensionsDefault('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions');
assert(compareFileExtensionsDefault('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files');
// numeric comparisons
assert(compareFileExtensionsNumeric('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order');
assert(compareFileExtensionsNumeric('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale');
assert(compareFileExtensionsNumeric('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first');
assert(compareFileExtensionsNumeric('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first');
assert(compareFileExtensionsDefault('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order');
assert(compareFileExtensionsDefault('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale');
assert(compareFileExtensionsDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first');
assert(compareFileExtensionsDefault('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first');
});
});
+23
View File
@@ -93,6 +93,22 @@ suite('dom', () => {
assert(!div.firstChild);
});
test('should buld nodes with id', () => {
const div = $('div#foo');
assert(div);
assert(div instanceof HTMLElement);
assert.equal(div.tagName, 'DIV');
assert.equal(div.id, 'foo');
});
test('should buld nodes with class-name', () => {
const div = $('div.foo');
assert(div);
assert(div instanceof HTMLElement);
assert.equal(div.tagName, 'DIV');
assert.equal(div.className, 'foo');
});
test('should build nodes with attributes', () => {
let div = $('div', { class: 'test' });
assert.equal(div.className, 'test');
@@ -111,5 +127,12 @@ suite('dom', () => {
assert.equal(div.firstChild && div.firstChild.textContent, 'hello');
});
test('should build nodes with text children', () => {
let div = $('div', undefined, 'foobar');
let firstChild = div.firstChild as HTMLElement;
assert.equal(firstChild.tagName, undefined);
assert.equal(firstChild.textContent, 'foobar');
});
});
});
@@ -18,7 +18,6 @@ suite('MarkdownRenderer', () => {
const result: HTMLElement = renderMarkdown(markdown);
const renderer = new marked.Renderer();
const imageFromMarked = marked(markdown.value, {
sanitize: true,
renderer
}).trim();
assert.strictEqual(result.innerHTML, imageFromMarked);
@@ -29,7 +28,6 @@ suite('MarkdownRenderer', () => {
const result: HTMLElement = renderMarkdown(markdown);
const renderer = new marked.Renderer();
const imageFromMarked = marked(markdown.value, {
sanitize: true,
renderer
}).trim();
assert.strictEqual(result.innerHTML, imageFromMarked);
+108 -8
View File
@@ -110,10 +110,10 @@ suite('Fuzzy Scorer', () => {
scores.push(_doScore(target, 'hw', true)); // direct mix-case prefix (multiple)
scores.push(_doScore(target, 'H', true)); // direct case prefix
scores.push(_doScore(target, 'h', true)); // direct mix-case prefix
scores.push(_doScore(target, 'ld', true)); // in-string mix-case match (consecutive, avoids scattered hit)
scores.push(_doScore(target, 'W', true)); // direct case word prefix
scores.push(_doScore(target, 'w', true)); // direct mix-case word prefix
scores.push(_doScore(target, 'Ld', true)); // in-string case match (multiple)
scores.push(_doScore(target, 'ld', true)); // in-string mix-case match (consecutive, avoids scattered hit)
scores.push(_doScore(target, 'w', true)); // direct mix-case word prefix
scores.push(_doScore(target, 'L', true)); // in-string case match
scores.push(_doScore(target, 'l', true)); // in-string mix-case match
scores.push(_doScore(target, '4', true)); // no match
@@ -123,13 +123,13 @@ suite('Fuzzy Scorer', () => {
assert.deepEqual(scores, sortedScores);
// Assert scoring positions
let positions = scores[0][1];
assert.equal(positions.length, 'HelLo-World'.length);
// let positions = scores[0][1];
// assert.equal(positions.length, 'HelLo-World'.length);
positions = scores[2][1];
assert.equal(positions.length, 'HW'.length);
assert.equal(positions[0], 0);
assert.equal(positions[1], 6);
// positions = scores[2][1];
// assert.equal(positions.length, 'HW'.length);
// assert.equal(positions[0], 0);
// assert.equal(positions[1], 6);
});
test('score (non fuzzy)', function () {
@@ -626,6 +626,21 @@ suite('Fuzzy Scorer', () => {
assert.equal(res[1], resourceA);
});
test('compareFilesByScore - prefer camel case matches', function () {
const resourceA = URI.file('config/test/NullPointerException.java');
const resourceB = URI.file('config/test/nopointerexception.java');
for (const query of ['npe', 'NPE']) {
let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor));
assert.equal(res[0], resourceA);
assert.equal(res[1], resourceB);
res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor));
assert.equal(res[0], resourceA);
assert.equal(res[1], resourceB);
}
});
test('compareFilesByScore - prefer more compact camel case matches', function () {
const resourceA = URI.file('config/test/openthisAnythingHandler.js');
const resourceB = URI.file('config/test/openthisisnotsorelevantforthequeryAnyHand.js');
@@ -925,6 +940,91 @@ suite('Fuzzy Scorer', () => {
assert.equal(res[0], resourceB);
});
test('compareFilesByScore - prefer shorter match (bug #103052) - foo bar', function () {
const resourceA = URI.file('app/emails/foo.bar.js');
const resourceB = URI.file('app/emails/other-footer.other-bar.js');
for (const query of ['foo bar', 'foobar']) {
let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor));
assert.equal(res[0], resourceA);
assert.equal(res[1], resourceB);
res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor));
assert.equal(res[0], resourceA);
assert.equal(res[1], resourceB);
}
});
test('compareFilesByScore - prefer shorter match (bug #103052) - payment model', function () {
const resourceA = URI.file('app/components/payment/payment.model.js');
const resourceB = URI.file('app/components/online-payments-history/online-payments-history.model.js');
for (const query of ['payment model', 'paymentmodel']) {
let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor));
assert.equal(res[0], resourceA);
assert.equal(res[1], resourceB);
res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor));
assert.equal(res[0], resourceA);
assert.equal(res[1], resourceB);
}
});
test('compareFilesByScore - prefer shorter match (bug #103052) - color', function () {
const resourceA = URI.file('app/constants/color.js');
const resourceB = URI.file('app/components/model/input/pick-avatar-color.js');
for (const query of ['color js', 'colorjs']) {
let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor));
assert.equal(res[0], resourceA);
assert.equal(res[1], resourceB);
res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor));
assert.equal(res[0], resourceA);
assert.equal(res[1], resourceB);
}
});
test('compareFilesByScore - prefer strict case prefix', function () {
const resourceA = URI.file('app/constants/color.js');
const resourceB = URI.file('app/components/model/input/Color.js');
let query = 'Color';
let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor));
assert.equal(res[0], resourceB);
assert.equal(res[1], resourceA);
res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor));
assert.equal(res[0], resourceB);
assert.equal(res[1], resourceA);
query = 'color';
res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor));
assert.equal(res[0], resourceA);
assert.equal(res[1], resourceB);
res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor));
assert.equal(res[0], resourceA);
assert.equal(res[1], resourceB);
});
test('compareFilesByScore - prefer prefix (bug #103052)', function () {
const resourceA = URI.file('test/smoke/src/main.ts');
const resourceB = URI.file('src/vs/editor/common/services/semantikTokensProviderStyling.ts');
let query = 'smoke main.ts';
let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor));
assert.equal(res[0], resourceA);
assert.equal(res[1], resourceB);
res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor));
assert.equal(res[0], resourceA);
assert.equal(res[1], resourceB);
});
test('prepareQuery', () => {
assert.equal(scorer.prepareQuery(' f*a ').normalized, 'fa');
assert.equal(scorer.prepareQuery('model Tester.ts').original, 'model Tester.ts');
+21 -3
View File
@@ -32,12 +32,18 @@ suite('Hash', () => {
assert.equal(hash([1, 2, 3]), hash([1, 2, 3]));
assert.equal(hash(['foo', 'bar']), hash(['foo', 'bar']));
assert.equal(hash([]), hash([]));
assert.equal(hash([]), hash(new Array()));
assert.notEqual(hash(['foo', 'bar']), hash(['bar', 'foo']));
assert.notEqual(hash(['foo', 'bar']), hash(['bar', 'foo', null]));
assert.notEqual(hash(['foo', 'bar', null]), hash(['bar', 'foo', null]));
assert.notEqual(hash(['foo', 'bar']), hash(['bar', 'foo', undefined]));
assert.notEqual(hash(['foo', 'bar', undefined]), hash(['bar', 'foo', undefined]));
assert.notEqual(hash(['foo', 'bar', null]), hash(['foo', 'bar', undefined]));
});
test('object', () => {
assert.equal(hash({}), hash({}));
assert.equal(hash({}), hash(Object.create(null)));
assert.equal(hash({ 'foo': 'bar' }), hash({ 'foo': 'bar' }));
assert.equal(hash({ 'foo': 'bar', 'foo2': undefined }), hash({ 'foo2': undefined, 'foo': 'bar' }));
assert.notEqual(hash({ 'foo': 'bar' }), hash({ 'foo': 'bar2' }));
@@ -45,14 +51,26 @@ suite('Hash', () => {
});
test('array - unexpected collision', function () {
this.skip();
const a = hash([undefined, undefined, undefined, undefined, undefined]);
const b = hash([undefined, undefined, 'HHHHHH', [{ line: 0, character: 0 }, { line: 0, character: 0 }], undefined]);
// console.log(a);
// console.log(b);
assert.notEqual(a, b);
});
test('all different', () => {
const candidates: any[] = [
null, undefined, {}, [], 0, false, true, '', ' ', [null], [undefined], [undefined, undefined], { '': undefined }, { [' ']: undefined },
'ab', 'ba', ['ab']
];
const hashes: number[] = candidates.map(hash);
for (let i = 0; i < hashes.length; i++) {
assert.equal(hashes[i], hash(candidates[i])); // verify that repeated invocation returns the same hash
for (let k = i + 1; k < hashes.length; k++) {
assert.notEqual(hashes[i], hashes[k], `Same hash ${hashes[i]} for ${JSON.stringify(candidates[i])} and ${JSON.stringify(candidates[k])}`);
}
}
});
function checkSHA1(strings: string[], expected: string) {
const hash = new StringSHA1();
for (const str of strings) {
+88 -1
View File
@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { IDisposable, dispose, ReferenceCollection } from 'vs/base/common/lifecycle';
import { DisposableStore, dispose, IDisposable, MultiDisposeError, ReferenceCollection, toDisposable } from 'vs/base/common/lifecycle';
class Disposable implements IDisposable {
isDisposed = false;
@@ -49,6 +49,48 @@ suite('Lifecycle', () => {
assert(disposable2.isDisposed);
});
test('dispose array should dispose all if a child throws on dispose', () => {
const disposedValues = new Set<number>();
let thrownError: any;
try {
dispose([
toDisposable(() => { disposedValues.add(1); }),
toDisposable(() => { throw new Error('I am error'); }),
toDisposable(() => { disposedValues.add(3); }),
]);
} catch (e) {
thrownError = e;
}
assert.ok(disposedValues.has(1));
assert.ok(disposedValues.has(3));
assert.strictEqual(thrownError.message, 'I am error');
});
test('dispose array should rethrow composite error if multiple entries throw on dispose', () => {
const disposedValues = new Set<number>();
let thrownError: any;
try {
dispose([
toDisposable(() => { disposedValues.add(1); }),
toDisposable(() => { throw new Error('I am error 1'); }),
toDisposable(() => { throw new Error('I am error 2'); }),
toDisposable(() => { disposedValues.add(4); }),
]);
} catch (e) {
thrownError = e;
}
assert.ok(disposedValues.has(1));
assert.ok(disposedValues.has(4));
assert.ok(thrownError instanceof MultiDisposeError);
assert.strictEqual((thrownError as MultiDisposeError).errors.length, 2);
assert.strictEqual((thrownError as MultiDisposeError).errors[0].message, 'I am error 1');
assert.strictEqual((thrownError as MultiDisposeError).errors[1].message, 'I am error 2');
});
test('Action bar has broken accessibility #100273', function () {
let array = [{ dispose() { } }, { dispose() { } }];
let array2 = dispose(array);
@@ -61,7 +103,52 @@ suite('Lifecycle', () => {
let setValues = set.values();
let setValues2 = dispose(setValues);
assert.ok(setValues === setValues2);
});
});
suite('DisposableStore', () => {
test('dispose should call all child disposes even if a child throws on dispose', () => {
const disposedValues = new Set<number>();
const store = new DisposableStore();
store.add(toDisposable(() => { disposedValues.add(1); }));
store.add(toDisposable(() => { throw new Error('I am error'); }));
store.add(toDisposable(() => { disposedValues.add(3); }));
let thrownError: any;
try {
store.dispose();
} catch (e) {
thrownError = e;
}
assert.ok(disposedValues.has(1));
assert.ok(disposedValues.has(3));
assert.strictEqual(thrownError.message, 'I am error');
});
test('dispose should throw composite error if multiple children throw on dispose', () => {
const disposedValues = new Set<number>();
const store = new DisposableStore();
store.add(toDisposable(() => { disposedValues.add(1); }));
store.add(toDisposable(() => { throw new Error('I am error 1'); }));
store.add(toDisposable(() => { throw new Error('I am error 2'); }));
store.add(toDisposable(() => { disposedValues.add(4); }));
let thrownError: any;
try {
store.dispose();
} catch (e) {
thrownError = e;
}
assert.ok(disposedValues.has(1));
assert.ok(disposedValues.has(4));
assert.ok(thrownError instanceof MultiDisposeError);
assert.strictEqual((thrownError as MultiDisposeError).errors.length, 2);
assert.strictEqual((thrownError as MultiDisposeError).errors[0].message, 'I am error 1');
assert.strictEqual((thrownError as MultiDisposeError).errors[1].message, 'I am error 2');
});
});
-1
View File
@@ -224,7 +224,6 @@ suite('PFS', function () {
}
catch (error) {
assert.fail(error);
throw error;
}
});
@@ -30,7 +30,6 @@
<!-- Startup (do not modify order of script tags!) -->
<script>
// NOTE: Changes to inline scripts require update of content security policy
self.require = {
baseUrl: `${window.location.origin}/static/out`,
recordStats: true,
@@ -101,7 +100,6 @@
globalThis.MonacoPerformanceMarks.push('willLoadWorkbenchMain', Date.now());
</script>
<script>
// NOTE: Changes to inline scripts require update of content security policy
require(['vs/code/browser/workbench/workbench'], function() {});
</script>
</html>
@@ -31,7 +31,6 @@
<!-- Startup (do not modify order of script tags!) -->
<script>
// NOTE: Changes to inline scripts require update of content security policy
self.require = {
baseUrl: `${window.location.origin}/static/out`,
recordStats: true,
+84 -6
View File
@@ -3,7 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IWorkbenchConstructionOptions, create, URI, Emitter, UriComponents, ICredentialsProvider, IURLCallbackProvider, IWorkspaceProvider, IWorkspace } from 'vs/workbench/workbench.web.api';
import { IWorkbenchConstructionOptions, create, ICredentialsProvider, IURLCallbackProvider, IWorkspaceProvider, IWorkspace, IWindowIndicator, ICommand, IHomeIndicator, IProductQualityChangeHandler } from 'vs/workbench/workbench.web.api';
import product from 'vs/platform/product/common/product';
import { URI, UriComponents } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { generateUuid } from 'vs/base/common/uuid';
import { CancellationToken } from 'vs/base/common/cancellation';
import { streamToBuffer } from 'vs/base/common/buffer';
@@ -276,6 +279,51 @@ class WorkspaceProvider implements IWorkspaceProvider {
}
}
class WindowIndicator implements IWindowIndicator {
readonly onDidChange = Event.None;
readonly label: string;
readonly tooltip: string;
readonly command: string | undefined;
readonly commandImpl: ICommand | undefined = undefined;
constructor(workspace: IWorkspace) {
let repositoryOwner: string | undefined = undefined;
let repositoryName: string | undefined = undefined;
if (workspace) {
let uri: URI | undefined = undefined;
if (isFolderToOpen(workspace)) {
uri = workspace.folderUri;
} else if (isWorkspaceToOpen(workspace)) {
uri = workspace.workspaceUri;
}
if (uri?.scheme === 'github' || uri?.scheme === 'codespace') {
[repositoryOwner, repositoryName] = uri.authority.split('+');
}
}
if (repositoryName && repositoryOwner) {
this.label = localize('openInDesktopLabel', "$(remote) Open in Desktop");
this.tooltip = localize('openInDesktopTooltip', "Open in Desktop");
this.command = '_web.openInDesktop';
this.commandImpl = {
id: this.command,
handler: () => {
const protocol = product.quality === 'stable' ? 'vscode' : 'vscode-insiders';
window.open(`${protocol}://vscode.git/clone?url=${encodeURIComponent(`https://github.com/${repositoryOwner}/${repositoryName}.git`)}`);
}
};
} else {
this.label = localize('playgroundLabel', "Web Playground");
this.tooltip = this.label;
}
}
}
(function () {
// Find config by checking for DOM
@@ -343,14 +391,44 @@ class WorkspaceProvider implements IWorkspaceProvider {
}
}
// Home Indicator
const homeIndicator: IHomeIndicator = {
href: 'https://github.com/Microsoft/vscode',
icon: 'code',
title: localize('home', "Home")
};
// Commands
const commands: ICommand[] = [];
// Window indicator
const windowIndicator = new WindowIndicator(workspace);
if (windowIndicator.commandImpl) {
commands.push(windowIndicator.commandImpl);
}
// Product Quality Change Handler
const productQualityChangeHandler: IProductQualityChangeHandler = (quality) => {
let queryString = `quality=${quality}`;
// Save all other query params we might have
const query = new URL(document.location.href).searchParams;
query.forEach((value, key) => {
if (key !== 'quality') {
queryString += `&${key}=${value}`;
}
});
window.location.href = `${window.location.origin}?${queryString}`;
};
// Finally create workbench
create(document.body, {
...config,
homeIndicator: {
href: 'https://github.com/Microsoft/vscode',
icon: 'code',
title: localize('home', "Home")
},
homeIndicator,
commands,
windowIndicator,
productQualityChangeHandler,
workspaceProvider: new WorkspaceProvider(workspace, payload),
urlCallbackProvider: new PollingURLCallbackProvider(),
credentialsProvider: new LocalStorageCredentialsProvider()
+33 -6
View File
@@ -82,6 +82,10 @@ import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMai
import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService';
import { createServer, AddressInfo } from 'net';
import { IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
import { IFileService } from 'vs/platform/files/common/files';
import { stripComments } from 'vs/base/common/json';
import { generateUuid } from 'vs/base/common/uuid';
import { VSBuffer } from 'vs/base/common/buffer';
export class CodeApplication extends Disposable {
private windowsMainService: IWindowsMainService | undefined;
@@ -134,11 +138,6 @@ export class CodeApplication extends Disposable {
//
// !!! DO NOT CHANGE without consulting the documentation !!!
//
app.on('remote-get-guest-web-contents', event => {
this.logService.trace('App#on(remote-get-guest-web-contents): prevented');
event.preventDefault();
});
app.on('remote-require', (event, sender, module) => {
this.logService.trace('App#on(remote-require): prevented');
@@ -807,7 +806,7 @@ export class CodeApplication extends Disposable {
return { fileUri: URI.file(path) };
}
private afterWindowOpen(accessor: ServicesAccessor): void {
private async afterWindowOpen(accessor: ServicesAccessor): Promise<void> {
// Signal phase: after window open
this.lifecycleMainService.phase = LifecycleMainPhase.AfterWindowOpen;
@@ -820,6 +819,34 @@ export class CodeApplication extends Disposable {
if (updateService instanceof Win32UpdateService || updateService instanceof LinuxUpdateService || updateService instanceof DarwinUpdateService) {
updateService.initialize();
}
// If enable-crash-reporter argv is undefined then this is a fresh start,
// based on telemetry.enableCrashreporter settings, generate a UUID which
// will be used as crash reporter id and also update the json file.
try {
const fileService = accessor.get(IFileService);
const argvContent = await fileService.readFile(this.environmentService.argvResource);
const argvString = argvContent.value.toString();
const argvJSON = JSON.parse(stripComments(argvString));
if (argvJSON['enable-crash-reporter'] === undefined) {
const enableCrashReporter = this.configurationService.getValue<boolean>('telemetry.enableCrashReporter') ?? true;
const additionalArgvContent = [
'',
' // Allows to disable crash reporting.',
' // Should restart the app if the value is changed.',
` "enable-crash-reporter": ${enableCrashReporter},`,
'',
' // Unique id used for correlating crash reports sent from this instance.',
' // Do not edit this value.',
` "crash-reporter-id": "${generateUuid()}"`,
'}'
];
const newArgvString = argvString.substring(0, argvString.length - 2).concat(',\n', additionalArgvContent.join('\n'));
await fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(newArgvString));
}
} catch (error) {
this.logService.error(error);
}
}
private handleRemoteAuthorities(): void {
+6 -2
View File
@@ -289,7 +289,8 @@ class CodeMain {
// Process Info
if (args.status) {
return instantiationService.invokeFunction(async accessor => {
return instantiationService.invokeFunction(async () => {
// Create a diagnostic service connected to the existing shared process
const sharedProcessClient = await connect(environmentService.sharedIPCHandle, 'main');
const diagnosticsChannel = sharedProcessClient.getChannel('diagnostics');
@@ -357,7 +358,10 @@ class CodeMain {
}
private showStartupWarningDialog(message: string, detail: string): void {
dialog.showMessageBox({
// use sync variant here because we likely exit after this method
// due to startup issues and otherwise the dialog seems to disappear
// https://github.com/microsoft/vscode/issues/104493
dialog.showMessageBoxSync({
title: product.nameLong,
type: 'warning',
buttons: [mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
@@ -553,7 +553,7 @@ export class IssueReporter extends Disposable {
private clearSearchResults(): void {
const similarIssues = this.getElementById('similar-issues')!;
similarIssues.innerHTML = '';
similarIssues.innerText = '';
this.numberOfSearchResultsDisplayed = 0;
}
@@ -564,7 +564,7 @@ export class IssueReporter extends Disposable {
window.fetch(`https://api.github.com/search/issues?q=${query}`).then((response) => {
response.json().then(result => {
similarIssues.innerHTML = '';
similarIssues.innerText = '';
if (result && result.items) {
this.displaySearchResults(result.items);
} else {
@@ -713,7 +713,7 @@ export class IssueReporter extends Disposable {
}
}
sourceSelect.innerHTML = '';
sourceSelect.innerText = '';
if (issueType === IssueType.FeatureRequest) {
sourceSelect.append(...[
this.makeOption('', localize('selectSource', "Select source"), true),
@@ -812,11 +812,14 @@ export class IssueReporter extends Disposable {
private validateInput(inputId: string): boolean {
const inputElement = (<HTMLInputElement>this.getElementById(inputId));
const inputValidationMessage = this.getElementById(`${inputId}-empty-error`);
if (!inputElement.value) {
inputElement.classList.add('invalid-input');
inputValidationMessage?.classList.remove('hidden');
return false;
} else {
inputElement.classList.remove('invalid-input');
inputValidationMessage?.classList.add('hidden');
return true;
}
}
@@ -1079,7 +1082,7 @@ export class IssueReporter extends Disposable {
}
private updateExtensionTable(extensions: IssueReporterExtensionData[], numThemeExtensions: number): void {
const target = document.querySelector('.block-extensions .block-info');
const target = document.querySelector<HTMLElement>('.block-extensions .block-info');
if (target) {
if (this.configuration.disableExtensions) {
target.innerHTML = localize('disabledExtensions', "Extensions are disabled");
@@ -1090,7 +1093,7 @@ export class IssueReporter extends Disposable {
extensions = extensions || [];
if (!extensions.length) {
target.innerHTML = 'Extensions: none' + themeExclusionStr;
target.innerText = 'Extensions: none' + themeExclusionStr;
return;
}
@@ -1100,10 +1103,10 @@ export class IssueReporter extends Disposable {
}
private updateSearchedExtensionTable(extensions: IssueReporterExtensionData[]): void {
const target = document.querySelector('.block-searchedExtensions .block-info');
const target = document.querySelector<HTMLElement>('.block-searchedExtensions .block-info');
if (target) {
if (!extensions.length) {
target.innerHTML = 'Extensions: none';
target.innerText = 'Extensions: none';
return;
}
@@ -23,6 +23,7 @@ export default (): string => `
<select id="issue-source" class="inline-form-control" required>
<!-- To be dynamically filled -->
</select>
<div id="issue-source-empty-error" class="validation-error hidden" role="alert">${escape(localize('issueSourceEmptyValidation', "An issue source is required."))}</div>
<div id="problem-source-help-text" class="instructions hidden">${escape(localize('disableExtensionsLabelText', "Try to reproduce the problem after {0}. If the problem only reproduces when extensions are active, it is likely an issue with an extension."))
.replace('{0}', `<span tabIndex=0 role="button" id="disableExtensions" class="workbenchCommand">${escape(localize('disableExtensions', "disabling all extensions and reloading the window"))}</span>`)}
</div>
@@ -43,6 +44,7 @@ export default (): string => `
<div class="input-group">
<label class="inline-label" for="issue-title">${escape(localize('issueTitleLabel', "Title"))} <span class="required-input">*</span></label>
<input id="issue-title" type="text" class="inline-form-control" placeholder="${escape(localize('issueTitleRequired', "Please enter a title."))}" required>
<div id="issue-title-empty-error" class="validation-error hidden" role="alert">${escape(localize('titleEmptyValidation', "A title is required."))}</div>
<div id="issue-title-length-validation-error" class="validation-error hidden" role="alert">${escape(localize('titleLengthValidation', "The title is too long."))}</div>
<small id="similar-issues">
<!-- To be dynamically filled -->
@@ -61,6 +63,7 @@ export default (): string => `
<div class="block-info-text">
<textarea name="description" id="description" placeholder="${escape(localize('details', "Please enter details."))}" required></textarea>
</div>
<div id="description-empty-error" class="validation-error hidden" role="alert">${escape(localize('descriptionEmptyValidation', "A description is required."))}</div>
</div>
<div class="system-info" id="block-container">
@@ -201,9 +201,10 @@ select, input, textarea {
}
.validation-error {
#issue-reporter .validation-error {
font-size: 12px;
margin-top: 1em;
padding: 10px;
border-top: 0px !important;
}
@@ -256,8 +257,7 @@ a {
}
.section .input-group .validation-error {
margin-left: calc(15% + 5px);
padding: 10px;
margin-left: 100px;
}
.section .inline-form-control, .section .inline-label {
@@ -268,7 +268,7 @@ a {
width: 95px;
}
.section .inline-form-control {
.section .inline-form-control, .section .input-group .validation-error {
width: calc(100% - 100px);
}
@@ -294,9 +294,13 @@ a {
margin-left: calc(15% + 1em);
}
.section .inline-form-control {
.section .inline-form-control, .section .input-group .validation-error {
width: calc(85% - 5px);
}
.section .input-group .validation-error {
margin-left: calc(15% + 4px);
}
}
@media (max-width: 620px) {
@@ -308,7 +312,7 @@ a {
margin-left: 1em;
}
.section .inline-form-control {
.section .inline-form-control, .section .input-group .validation-error {
width: 100%;
}
@@ -267,7 +267,7 @@ class ProcessExplorer {
return;
}
container.innerHTML = '';
container.innerText = '';
this.listeners.clear();
const tableHead = document.createElement('thead');
@@ -124,12 +124,12 @@ class DomCharWidthReader {
private static _render(testElement: HTMLElement, request: CharWidthRequest): void {
if (request.chr === ' ') {
let htmlString = '&#160;';
let htmlString = '\u00a0';
// Repeat character 256 (2^8) times
for (let i = 0; i < 8; i++) {
htmlString += htmlString;
}
testElement.innerHTML = htmlString;
testElement.innerText = htmlString;
} else {
let testString = request.chr;
// Repeat character 256 (2^8) times
@@ -559,9 +559,9 @@ export namespace CoreNavigationCommands {
case CursorMove_.Direction.ViewPortCenter:
case CursorMove_.Direction.ViewPortIfOutside:
return CursorMoveCommands.viewportMove(viewModel, cursors, args.direction, inSelectionMode, value);
default:
return null;
}
return null;
}
}
@@ -178,14 +178,7 @@ export class TextAreaHandler extends ViewPart {
mode
};
},
getScreenReaderContent: (currentState: TextAreaState): TextAreaState => {
if (browser.isIPad) {
// Do not place anything in the textarea for the iPad
return TextAreaState.EMPTY;
}
if (this._accessibilitySupport === AccessibilitySupport.Disabled) {
// We know for a fact that a screen reader is not attached
// On OSX, we write the character before the cursor to allow for "long-press" composition
+16
View File
@@ -580,6 +580,11 @@ export interface ICodeEditor extends editorCommon.IEditor {
*/
getRawOptions(): IEditorOptions;
/**
* @internal
*/
getOverflowWidgetsDomNode(): HTMLElement | undefined;
/**
* @internal
*/
@@ -1055,3 +1060,14 @@ export function getCodeEditor(thing: any): ICodeEditor | null {
return null;
}
/**
*@internal
*/
export function getIEditor(thing: any): editorCommon.IEditor | null {
if (isCodeEditor(thing) || isDiffEditor(thing)) {
return thing;
}
return null;
}
+4 -2
View File
@@ -109,8 +109,9 @@ export class ViewController {
return data.ctrlKey;
case 'metaKey':
return data.metaKey;
default:
return false;
}
return false;
}
private _hasNonMulticursorModifier(data: IMouseDispatchData): boolean {
@@ -121,8 +122,9 @@ export class ViewController {
return data.altKey || data.metaKey;
case 'metaKey':
return data.ctrlKey || data.altKey;
default:
return false;
}
return false;
}
public dispatchMouse(data: IMouseDispatchData): void {
@@ -379,6 +379,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
return this._configuration.getRawOptions();
}
public getOverflowWidgetsDomNode(): HTMLElement | undefined {
return this._overflowWidgetsDomNode;
}
public getConfiguredWordAtPosition(position: Position): IWordAtPosition | null {
if (!this._modelData) {
return null;
+4 -4
View File
@@ -702,7 +702,7 @@ export class DiffReview extends Disposable {
if (originalLine !== 0) {
originalLineNumber.appendChild(document.createTextNode(String(originalLine)));
} else {
originalLineNumber.innerHTML = '&#160;';
originalLineNumber.innerText = '\u00a0';
}
cell.appendChild(originalLineNumber);
@@ -714,7 +714,7 @@ export class DiffReview extends Disposable {
if (modifiedLine !== 0) {
modifiedLineNumber.appendChild(document.createTextNode(String(modifiedLine)));
} else {
modifiedLineNumber.innerHTML = '&#160;';
modifiedLineNumber.innerText = '\u00a0';
}
cell.appendChild(modifiedLineNumber);
@@ -724,10 +724,10 @@ export class DiffReview extends Disposable {
if (spacerIcon) {
const spacerCodicon = document.createElement('span');
spacerCodicon.className = spacerIcon.classNames;
spacerCodicon.innerHTML = '&#160;&#160;';
spacerCodicon.innerText = '\u00a0\u00a0';
spacer.appendChild(spacerCodicon);
} else {
spacer.innerHTML = '&#160;&#160;';
spacer.innerText = '\u00a0\u00a0';
}
cell.appendChild(spacer);
@@ -37,7 +37,7 @@ export class EmbeddedCodeEditorWidget extends CodeEditorWidget {
@INotificationService notificationService: INotificationService,
@IAccessibilityService accessibilityService: IAccessibilityService
) {
super(domElement, parentEditor.getRawOptions(), {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService);
super(domElement, { ...parentEditor.getRawOptions(), overflowWidgetsDomNode: parentEditor.getOverflowWidgetsDomNode() }, {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService);
this._parentEditor = parentEditor;
this._overwriteOptions = options;
@@ -1286,6 +1286,10 @@ class EditorEmptySelectionClipboard extends EditorBooleanOption<EditorOption.emp
* Configuration options for editor find widget
*/
export interface IEditorFindOptions {
/**
* Controls whether the cursor should move to find matches while typing.
*/
cursorMoveOnType?: boolean;
/**
* Controls if we seed search string in the Find Widget with editor selection.
*/
@@ -1315,6 +1319,7 @@ class EditorFind extends BaseEditorOption<EditorOption.find, EditorFindOptions>
constructor() {
const defaults: EditorFindOptions = {
cursorMoveOnType: true,
seedSearchStringFromSelection: true,
autoFindInSelection: 'never',
globalFindClipboard: false,
@@ -1324,6 +1329,11 @@ class EditorFind extends BaseEditorOption<EditorOption.find, EditorFindOptions>
super(
EditorOption.find, 'find', defaults,
{
'editor.find.cursorMoveOnType': {
type: 'boolean',
default: defaults.cursorMoveOnType,
description: nls.localize('find.cursorMoveOnType', "Controls whether the cursor should jump to find matches while typing.")
},
'editor.find.seedSearchStringFromSelection': {
type: 'boolean',
default: defaults.seedSearchStringFromSelection,
@@ -1367,6 +1377,7 @@ class EditorFind extends BaseEditorOption<EditorOption.find, EditorFindOptions>
}
const input = _input as IEditorFindOptions;
return {
cursorMoveOnType: EditorBooleanOption.boolean(input.cursorMoveOnType, this.defaultValue.cursorMoveOnType),
seedSearchStringFromSelection: EditorBooleanOption.boolean(input.seedSearchStringFromSelection, this.defaultValue.seedSearchStringFromSelection),
autoFindInSelection: typeof _input.autoFindInSelection === 'boolean'
? (_input.autoFindInSelection ? 'always' : 'never')
@@ -317,9 +317,10 @@ export class CursorMoveCommands {
// Move to the last non-whitespace column of the current view line
return this._moveToViewLastNonWhitespaceColumn(viewModel, cursors, inSelectionMode);
}
default:
return null;
}
return null;
}
public static viewportMove(viewModel: IViewModel, cursors: CursorState[], direction: CursorMove.ViewportDirection, inSelectionMode: boolean, value: number): PartialCursorState[] | null {
@@ -353,9 +354,9 @@ export class CursorMoveCommands {
}
return result;
}
default:
return null;
}
return null;
}
public static findPositionInViewportIfOutside(viewModel: IViewModel, cursor: CursorState, visibleViewRange: Range, inSelectionMode: boolean): PartialCursorState {
+2 -2
View File
@@ -800,7 +800,7 @@ export interface ITextModel {
/**
* Search the model.
* @param searchString The string used to search. If it is a regular expression, set `isRegex` to true.
* @param searchScope Limit the searching to only search inside this range.
* @param searchScope Limit the searching to only search inside these ranges.
* @param isRegex Used to indicate that `searchString` is a regular expression.
* @param matchCase Force the matching to match lower/upper case exactly.
* @param wordSeparators Force the matching to match entire words only. Pass null otherwise.
@@ -808,7 +808,7 @@ export interface ITextModel {
* @param limitResultCount Limit the number of results
* @return The ranges where the matches are. It is empty if no matches have been found.
*/
findMatches(searchString: string, searchScope: IRange, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount?: number): FindMatch[];
findMatches(searchString: string, searchScope: IRange | IRange[], isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount?: number): FindMatch[];
/**
* Search the model for the next match. Loops to the beginning of the model if needed.
* @param searchString The string used to search. If it is a regular expression, set `isRegex` to true.
@@ -214,8 +214,9 @@ export class PieceTreeTextBuffer implements ITextBuffer, IDisposable {
return '\r\n';
case EndOfLinePreference.TextDefined:
return this.getEOL();
default:
throw new Error('Unknown EOL preference');
}
throw new Error('Unknown EOL preference');
}
public setEOL(newEOL: '\r\n' | '\n'): void {
+31 -7
View File
@@ -1121,13 +1121,35 @@ export class TextModel extends Disposable implements model.ITextModel {
public findMatches(searchString: string, rawSearchScope: any, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount: number = LIMIT_FIND_COUNT): model.FindMatch[] {
this._assertNotDisposed();
let searchRange: Range;
if (Range.isIRange(rawSearchScope)) {
searchRange = this.validateRange(rawSearchScope);
} else {
searchRange = this.getFullModelRange();
let searchRanges: Range[] | null = null;
if (rawSearchScope !== null) {
if (!Array.isArray(rawSearchScope)) {
rawSearchScope = [rawSearchScope];
}
if (rawSearchScope.every((searchScope: Range) => Range.isIRange(searchScope))) {
searchRanges = rawSearchScope.map((searchScope: Range) => this.validateRange(searchScope));
}
}
if (searchRanges === null) {
searchRanges = [this.getFullModelRange()];
}
searchRanges = searchRanges.sort((d1, d2) => d1.startLineNumber - d2.startLineNumber || d1.startColumn - d2.startColumn);
const uniqueSearchRanges: Range[] = [];
uniqueSearchRanges.push(searchRanges.reduce((prev, curr) => {
if (Range.areIntersecting(prev, curr)) {
return prev.plusRange(curr);
}
uniqueSearchRanges.push(prev);
return curr;
}));
let matchMapper: (value: Range, index: number, array: Range[]) => model.FindMatch[];
if (!isRegex && searchString.indexOf('\n') < 0) {
// not regex, not multi line
const searchParams = new SearchParams(searchString, isRegex, matchCase, wordSeparators);
@@ -1137,10 +1159,12 @@ export class TextModel extends Disposable implements model.ITextModel {
return [];
}
return this.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount);
matchMapper = (searchRange: Range) => this.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount);
} else {
matchMapper = (searchRange: Range) => TextModelSearch.findMatches(this, new SearchParams(searchString, isRegex, matchCase, wordSeparators), searchRange, captureMatches, limitResultCount);
}
return TextModelSearch.findMatches(this, new SearchParams(searchString, isRegex, matchCase, wordSeparators), searchRange, captureMatches, limitResultCount);
return uniqueSearchRanges.map(matchMapper).reduce((arr, matches: model.FindMatch[]) => arr.concat(matches), []);
}
public findNextMatch(searchString: string, rawSearchStart: IPosition, isRegex: boolean, matchCase: boolean, wordSeparators: string, captureMatches: boolean): model.FindMatch | null {
+2 -2
View File
@@ -813,12 +813,12 @@ export interface DocumentHighlightProvider {
*/
export interface OnTypeRenameProvider {
stopPattern?: RegExp;
wordPattern?: RegExp;
/**
* Provide a list of ranges that can be live-renamed together.
*/
provideOnTypeRenameRanges(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult<IRange[]>;
provideOnTypeRenameRanges(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult<{ ranges: IRange[]; wordPattern?: RegExp; }>;
}
/**
@@ -95,7 +95,7 @@ export class CodeLensContribution implements IEditorContribution {
.monaco-editor .codelens-decoration.${this._styleClassName} { height: ${height}px; line-height: ${lineHeight}px; font-size: ${fontSize}px; padding-right: ${Math.round(fontInfo.fontSize * 0.45)}px;}
.monaco-editor .codelens-decoration.${this._styleClassName} > a > .codicon { line-height: ${lineHeight}px; font-size: ${fontSize}px; }
`;
this._styleElement.innerHTML = newStyle;
this._styleElement.textContent = newStyle;
}
private _localDispose(): void {
@@ -470,5 +470,3 @@ registerEditorAction(class ShowLensesInCurrentLine extends EditorAction {
}
}
});
@@ -108,7 +108,7 @@ class CodeLensContentWidget implements IContentWidget {
} else {
// symbols and commands
if (!innerHtml) {
innerHtml = '&#160;';
innerHtml = '\u00a0';
}
this._domNode.innerHTML = innerHtml;
if (this._isEmpty && animate) {
+19 -9
View File
@@ -233,12 +233,22 @@ export class CommonFindController extends Disposable implements IEditorContribut
this._state.change({ searchScope: null }, true);
} else {
if (this._editor.hasModel()) {
let selection = this._editor.getSelection();
if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) {
selection = selection.setEndPosition(selection.endLineNumber - 1, this._editor.getModel().getLineMaxColumn(selection.endLineNumber - 1));
}
if (!selection.isEmpty()) {
this._state.change({ searchScope: selection }, true);
let selections = this._editor.getSelections();
selections.map(selection => {
if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) {
selection = selection.setEndPosition(
selection.endLineNumber - 1,
this._editor.getModel()!.getLineMaxColumn(selection.endLineNumber - 1)
);
}
if (!selection.isEmpty()) {
return selection;
}
return null;
}).filter(element => !!element);
if (selections.length) {
this._state.change({ searchScope: selections }, true);
}
}
}
@@ -299,9 +309,9 @@ export class CommonFindController extends Disposable implements IEditorContribut
}
if (opts.updateSearchScope) {
let currentSelection = this._editor.getSelection();
if (!currentSelection.isEmpty()) {
stateChanges.searchScope = currentSelection;
let currentSelections = this._editor.getSelections();
if (currentSelections.some(selection => !selection.isEmpty())) {
stateChanges.searchScope = currentSelections;
}
}
+27 -14
View File
@@ -17,7 +17,7 @@ export class FindDecorations implements IDisposable {
private readonly _editor: IActiveCodeEditor;
private _decorations: string[];
private _overviewRulerApproximateDecorations: string[];
private _findScopeDecorationId: string | null;
private _findScopeDecorationIds: string[];
private _rangeHighlightDecorationId: string | null;
private _highlightedDecorationId: string | null;
private _startPosition: Position;
@@ -26,7 +26,7 @@ export class FindDecorations implements IDisposable {
this._editor = editor;
this._decorations = [];
this._overviewRulerApproximateDecorations = [];
this._findScopeDecorationId = null;
this._findScopeDecorationIds = [];
this._rangeHighlightDecorationId = null;
this._highlightedDecorationId = null;
this._startPosition = this._editor.getPosition();
@@ -37,7 +37,7 @@ export class FindDecorations implements IDisposable {
this._decorations = [];
this._overviewRulerApproximateDecorations = [];
this._findScopeDecorationId = null;
this._findScopeDecorationIds = [];
this._rangeHighlightDecorationId = null;
this._highlightedDecorationId = null;
}
@@ -45,7 +45,7 @@ export class FindDecorations implements IDisposable {
public reset(): void {
this._decorations = [];
this._overviewRulerApproximateDecorations = [];
this._findScopeDecorationId = null;
this._findScopeDecorationIds = [];
this._rangeHighlightDecorationId = null;
this._highlightedDecorationId = null;
}
@@ -54,9 +54,22 @@ export class FindDecorations implements IDisposable {
return this._decorations.length;
}
/** @deprecated use getFindScopes to support multiple selections */
public getFindScope(): Range | null {
if (this._findScopeDecorationId) {
return this._editor.getModel().getDecorationRange(this._findScopeDecorationId);
if (this._findScopeDecorationIds[0]) {
return this._editor.getModel().getDecorationRange(this._findScopeDecorationIds[0]);
}
return null;
}
public getFindScopes(): Range[] | null {
if (this._findScopeDecorationIds.length) {
const scopes = this._findScopeDecorationIds.map(findScopeDecorationId =>
this._editor.getModel().getDecorationRange(findScopeDecorationId)
).filter(element => !!element);
if (scopes.length) {
return scopes as Range[];
}
}
return null;
}
@@ -133,7 +146,7 @@ export class FindDecorations implements IDisposable {
return matchPosition;
}
public set(findMatches: FindMatch[], findScope: Range | null): void {
public set(findMatches: FindMatch[], findScopes: Range[] | null): void {
this._editor.changeDecorations((accessor) => {
let findMatchesOptions: ModelDecorationOptions = FindDecorations._FIND_MATCH_DECORATION;
@@ -195,12 +208,12 @@ export class FindDecorations implements IDisposable {
}
// Find scope
if (this._findScopeDecorationId) {
accessor.removeDecoration(this._findScopeDecorationId);
this._findScopeDecorationId = null;
if (this._findScopeDecorationIds.length) {
this._findScopeDecorationIds.forEach(findScopeDecorationId => accessor.removeDecoration(findScopeDecorationId));
this._findScopeDecorationIds = [];
}
if (findScope) {
this._findScopeDecorationId = accessor.addDecoration(findScope, FindDecorations._FIND_SCOPE_DECORATION);
if (findScopes?.length) {
this._findScopeDecorationIds = findScopes.map(findScope => accessor.addDecoration(findScope, FindDecorations._FIND_SCOPE_DECORATION));
}
});
}
@@ -253,8 +266,8 @@ export class FindDecorations implements IDisposable {
let result: string[] = [];
result = result.concat(this._decorations);
result = result.concat(this._overviewRulerApproximateDecorations);
if (this._findScopeDecorationId) {
result.push(this._findScopeDecorationId);
if (this._findScopeDecorationIds.length) {
result.push(...this._findScopeDecorationIds);
}
if (this._rangeHighlightDecorationId) {
result.push(this._rangeHighlightDecorationId);
+38 -25
View File
@@ -169,26 +169,36 @@ export class FindModelBoundToEditorModel {
return model.getFullModelRange();
}
private research(moveCursor: boolean, newFindScope?: Range | null): void {
let findScope: Range | null = null;
private research(moveCursor: boolean, newFindScope?: Range | Range[] | null): void {
let findScopes: Range[] | null = null;
if (typeof newFindScope !== 'undefined') {
findScope = newFindScope;
} else {
findScope = this._decorations.getFindScope();
}
if (findScope !== null) {
if (findScope.startLineNumber !== findScope.endLineNumber) {
if (findScope.endColumn === 1) {
findScope = new Range(findScope.startLineNumber, 1, findScope.endLineNumber - 1, this._editor.getModel().getLineMaxColumn(findScope.endLineNumber - 1));
if (newFindScope !== null) {
if (!Array.isArray(newFindScope)) {
findScopes = [newFindScope as Range];
} else {
// multiline find scope => expand to line starts / ends
findScope = new Range(findScope.startLineNumber, 1, findScope.endLineNumber, this._editor.getModel().getLineMaxColumn(findScope.endLineNumber));
findScopes = newFindScope;
}
}
} else {
findScopes = this._decorations.getFindScopes();
}
if (findScopes !== null) {
findScopes = findScopes.map(findScope => {
if (findScope.startLineNumber !== findScope.endLineNumber) {
let endLineNumber = findScope.endLineNumber;
if (findScope.endColumn === 1) {
endLineNumber = endLineNumber - 1;
}
return new Range(findScope.startLineNumber, 1, endLineNumber, this._editor.getModel().getLineMaxColumn(endLineNumber));
}
return findScope;
});
}
let findMatches = this._findMatches(findScope, false, MATCHES_LIMIT);
this._decorations.set(findMatches, findScope);
let findMatches = this._findMatches(findScopes, false, MATCHES_LIMIT);
this._decorations.set(findMatches, findScopes);
const editorSelection = this._editor.getSelection();
let currentMatchesPosition = this._decorations.getCurrentMatchesPosition(editorSelection);
@@ -205,7 +215,7 @@ export class FindModelBoundToEditorModel {
undefined
);
if (moveCursor) {
if (moveCursor && this._editor.getOption(EditorOption.find).cursorMoveOnType) {
this._moveToNextMatch(this._decorations.getStartPosition());
}
}
@@ -467,9 +477,12 @@ export class FindModelBoundToEditorModel {
}
}
private _findMatches(findScope: Range | null, captureMatches: boolean, limitResultCount: number): FindMatch[] {
let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), findScope);
return this._editor.getModel().findMatches(this._state.searchString, searchRange, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null, captureMatches, limitResultCount);
private _findMatches(findScopes: Range[] | null, captureMatches: boolean, limitResultCount: number): FindMatch[] {
const searchRanges = (findScopes as []).map((scope: Range | null) => // {{SQL CARBON EDIT}} strict-null-check
FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), scope)
);
return this._editor.getModel().findMatches(this._state.searchString, searchRanges, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null, captureMatches, limitResultCount);
}
public replaceAll(): void {
@@ -477,13 +490,13 @@ export class FindModelBoundToEditorModel {
return;
}
const findScope = this._decorations.getFindScope();
const findScopes = this._decorations.getFindScopes();
if (findScope === null && this._state.matchesCount >= MATCHES_LIMIT) {
if (findScopes === null && this._state.matchesCount >= MATCHES_LIMIT) {
// Doing a replace on the entire file that is over ${MATCHES_LIMIT} matches
this._largeReplaceAll();
} else {
this._regularReplaceAll(findScope);
this._regularReplaceAll(findScopes);
}
this.research(false);
@@ -528,10 +541,10 @@ export class FindModelBoundToEditorModel {
this._executeEditorCommand('replaceAll', command);
}
private _regularReplaceAll(findScope: Range | null): void {
private _regularReplaceAll(findScopes: Range[] | null): void {
const replacePattern = this._getReplacePattern();
// Get all the ranges (even more than the highlighted ones)
let matches = this._findMatches(findScope, replacePattern.hasReplacementPatterns || this._state.preserveCase, Constants.MAX_SAFE_SMALL_INTEGER);
let matches = this._findMatches(findScopes, replacePattern.hasReplacementPatterns || this._state.preserveCase, Constants.MAX_SAFE_SMALL_INTEGER);
let replaceStrings: string[] = [];
for (let i = 0, len = matches.length; i < len; i++) {
@@ -547,10 +560,10 @@ export class FindModelBoundToEditorModel {
return;
}
let findScope = this._decorations.getFindScope();
let findScopes = this._decorations.getFindScopes();
// Get all the ranges (even more than the highlighted ones)
let matches = this._findMatches(findScope, false, Constants.MAX_SAFE_SMALL_INTEGER);
let matches = this._findMatches(findScopes, false, Constants.MAX_SAFE_SMALL_INTEGER);
let selections = matches.map(m => new Selection(m.range.startLineNumber, m.range.startColumn, m.range.endLineNumber, m.range.endColumn));
// If one of the ranges is the editor selection, then maintain it as primary
+8 -4
View File
@@ -46,7 +46,7 @@ export interface INewFindReplaceState {
matchCaseOverride?: FindOptionOverride;
preserveCase?: boolean;
preserveCaseOverride?: FindOptionOverride;
searchScope?: Range | null;
searchScope?: Range[] | null;
loop?: boolean;
}
@@ -73,7 +73,7 @@ export class FindReplaceState extends Disposable {
private _matchCaseOverride: FindOptionOverride;
private _preserveCase: boolean;
private _preserveCaseOverride: FindOptionOverride;
private _searchScope: Range | null;
private _searchScope: Range[] | null;
private _matchesPosition: number;
private _matchesCount: number;
private _currentMatch: Range | null;
@@ -94,7 +94,7 @@ export class FindReplaceState extends Disposable {
public get actualMatchCase(): boolean { return this._matchCase; }
public get actualPreserveCase(): boolean { return this._preserveCase; }
public get searchScope(): Range | null { return this._searchScope; }
public get searchScope(): Range[] | null { return this._searchScope; }
public get matchesPosition(): number { return this._matchesPosition; }
public get matchesCount(): number { return this._matchesCount; }
public get currentMatch(): Range | null { return this._currentMatch; }
@@ -238,7 +238,11 @@ export class FindReplaceState extends Disposable {
this._preserveCase = newState.preserveCase;
}
if (typeof newState.searchScope !== 'undefined') {
if (!Range.equalsRange(this._searchScope, newState.searchScope)) {
if (!newState.searchScope?.every((newSearchScope) => {
return this._searchScope?.some(existingSearchScope => {
return !Range.equalsRange(existingSearchScope, newSearchScope);
});
})) {
this._searchScope = newState.searchScope;
changeEvent.searchScope = true;
somethingChanged = true;
+32 -15
View File
@@ -804,16 +804,26 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL
}
if (this._toggleSelectionFind.checked) {
let selection = this._codeEditor.getSelection();
if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) {
selection = selection.setEndPosition(selection.endLineNumber - 1, this._codeEditor.getModel().getLineMaxColumn(selection.endLineNumber - 1));
}
const currentMatch = this._state.currentMatch;
if (selection.startLineNumber !== selection.endLineNumber) {
if (!Range.equalsRange(selection, currentMatch)) {
// Reseed find scope
this._state.change({ searchScope: selection }, true);
let selections = this._codeEditor.getSelections();
selections.map(selection => {
if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) {
selection = selection.setEndPosition(
selection.endLineNumber - 1,
this._codeEditor.getModel()!.getLineMaxColumn(selection.endLineNumber - 1)
);
}
const currentMatch = this._state.currentMatch;
if (selection.startLineNumber !== selection.endLineNumber) {
if (!Range.equalsRange(selection, currentMatch)) {
return selection;
}
}
return null;
}).filter(element => !!element);
if (selections.length) {
this._state.change({ searchScope: selections as Range[] }, true);
}
}
}
@@ -1028,12 +1038,19 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL
this._register(this._toggleSelectionFind.onChange(() => {
if (this._toggleSelectionFind.checked) {
if (this._codeEditor.hasModel()) {
let selection = this._codeEditor.getSelection();
if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) {
selection = selection.setEndPosition(selection.endLineNumber - 1, this._codeEditor.getModel().getLineMaxColumn(selection.endLineNumber - 1));
}
if (!selection.isEmpty()) {
this._state.change({ searchScope: selection }, true);
let selections = this._codeEditor.getSelections();
selections.map(selection => {
if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) {
selection = selection.setEndPosition(selection.endLineNumber - 1, this._codeEditor.getModel()!.getLineMaxColumn(selection.endLineNumber - 1));
}
if (!selection.isEmpty()) {
return selection;
}
return null;
}).filter(element => !!element);
if (selections.length) {
this._state.change({ searchScope: selections as Range[] }, true);
}
}
} else {
@@ -309,10 +309,10 @@ suite.skip('FindController', async () => {
assert.equal(findController.getState().searchScope, null);
findController.getState().change({
searchScope: new Range(1, 1, 1, 5)
searchScope: [new Range(1, 1, 1, 5)]
}, false);
assert.deepEqual(findController.getState().searchScope, new Range(1, 1, 1, 5));
assert.deepEqual(findController.getState().searchScope, [new Range(1, 1, 1, 5)]);
findController.closeFindWidget();
assert.equal(findController.getState().searchScope, null);
@@ -523,10 +523,8 @@ suite.skip('FindController query options persistence', async () => {
'var z = (3 * 5)',
], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'always', globalFindClipboard: false } }, async (editor) => {
// clipboardState = '';
editor.setSelection(new Range(1, 1, 2, 1));
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
await findController.start({
const findConfig = {
forceRevealReplace: false,
seedSearchStringFromSelection: false,
seedSearchStringFromGlobalClipboard: false,
@@ -534,9 +532,17 @@ suite.skip('FindController query options persistence', async () => {
shouldAnimate: false,
updateSearchScope: true,
loop: true
});
};
assert.deepEqual(findController.getState().searchScope, new Selection(1, 1, 2, 1));
editor.setSelection(new Range(1, 1, 2, 1));
findController.start(findConfig);
assert.deepEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1)]);
findController.closeFindWidget();
editor.setSelections([new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]);
findController.start(findConfig);
assert.deepEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]);
});
});
@@ -584,7 +590,7 @@ suite.skip('FindController query options persistence', async () => {
loop: true
});
assert.deepEqual(findController.getState().searchScope, new Selection(1, 2, 1, 3));
assert.deepEqual(findController.getState().searchScope, [new Selection(1, 2, 1, 3)]);
});
});
@@ -609,7 +615,7 @@ suite.skip('FindController query options persistence', async () => {
loop: true
});
assert.deepEqual(findController.getState().searchScope, new Selection(1, 6, 2, 1));
assert.deepEqual(findController.getState().searchScope, [new Selection(1, 6, 2, 1)]);
});
});
});
@@ -210,7 +210,7 @@ suite('FindModel', () => {
);
// simulate adding a search scope
findState.change({ searchScope: new Range(8, 1, 10, 1) }, true);
findState.change({ searchScope: [new Range(8, 1, 10, 1)] }, true);
assertFindState(
editor,
[8, 14, 8, 19],
@@ -443,7 +443,7 @@ suite('FindModel', () => {
findTest('find model next stays in scope', (editor) => {
let findState = new FindReplaceState();
findState.change({ searchString: 'hello', wholeWord: true, searchScope: new Range(7, 1, 9, 1) }, false);
findState.change({ searchString: 'hello', wholeWord: true, searchScope: [new Range(7, 1, 9, 1)] }, false);
let findModel = new FindModelBoundToEditorModel(editor, findState);
assertFindState(
@@ -493,6 +493,131 @@ suite('FindModel', () => {
findState.dispose();
});
findTest('multi-selection find model next stays in scope (overlap)', (editor) => {
let findState = new FindReplaceState();
findState.change({ searchString: 'hello', wholeWord: true, searchScope: [new Range(7, 1, 8, 2), new Range(8, 1, 9, 1)] }, false);
let findModel = new FindModelBoundToEditorModel(editor, findState);
assertFindState(
editor,
[1, 1, 1, 1],
null,
[
[7, 14, 7, 19],
[8, 14, 8, 19]
]
);
findModel.moveToNextMatch();
assertFindState(
editor,
[7, 14, 7, 19],
[7, 14, 7, 19],
[
[7, 14, 7, 19],
[8, 14, 8, 19]
]
);
findModel.moveToNextMatch();
assertFindState(
editor,
[8, 14, 8, 19],
[8, 14, 8, 19],
[
[7, 14, 7, 19],
[8, 14, 8, 19]
]
);
findModel.moveToNextMatch();
assertFindState(
editor,
[7, 14, 7, 19],
[7, 14, 7, 19],
[
[7, 14, 7, 19],
[8, 14, 8, 19]
]
);
findModel.dispose();
findState.dispose();
});
findTest('multi-selection find model next stays in scope', (editor) => {
let findState = new FindReplaceState();
findState.change({ searchString: 'hello', matchCase: true, wholeWord: false, searchScope: [new Range(6, 1, 7, 38), new Range(9, 3, 9, 38)] }, false);
let findModel = new FindModelBoundToEditorModel(editor, findState);
assertFindState(
editor,
[1, 1, 1, 1],
null,
[
[6, 14, 6, 19],
// `matchCase: false` would
// find this match as well:
// [6, 27, 6, 32],
[7, 14, 7, 19],
// `wholeWord: true` would
// exclude this match:
[9, 14, 9, 19],
]
);
findModel.moveToNextMatch();
assertFindState(
editor,
[6, 14, 6, 19],
[6, 14, 6, 19],
[
[6, 14, 6, 19],
[7, 14, 7, 19],
[9, 14, 9, 19],
]
);
findModel.moveToNextMatch();
assertFindState(
editor,
[7, 14, 7, 19],
[7, 14, 7, 19],
[
[6, 14, 6, 19],
[7, 14, 7, 19],
[9, 14, 9, 19],
]
);
findModel.moveToNextMatch();
assertFindState(
editor,
[9, 14, 9, 19],
[9, 14, 9, 19],
[
[6, 14, 6, 19],
[7, 14, 7, 19],
[9, 14, 9, 19],
]
);
findModel.moveToNextMatch();
assertFindState(
editor,
[6, 14, 6, 19],
[6, 14, 6, 19],
[
[6, 14, 6, 19],
[7, 14, 7, 19],
[9, 14, 9, 19],
]
);
findModel.dispose();
findState.dispose();
});
findTest('find model prev', (editor) => {
let findState = new FindReplaceState();
findState.change({ searchString: 'hello', wholeWord: true }, false);
@@ -581,7 +706,7 @@ suite('FindModel', () => {
findTest('find model prev stays in scope', (editor) => {
let findState = new FindReplaceState();
findState.change({ searchString: 'hello', wholeWord: true, searchScope: new Range(7, 1, 9, 1) }, false);
findState.change({ searchString: 'hello', wholeWord: true, searchScope: [new Range(7, 1, 9, 1)] }, false);
let findModel = new FindModelBoundToEditorModel(editor, findState);
assertFindState(
@@ -2073,7 +2198,7 @@ suite('FindModel', () => {
findTest('issue #27083. search scope works even if it is a single line', (editor) => {
let findState = new FindReplaceState();
findState.change({ searchString: 'hello', wholeWord: true, searchScope: new Range(7, 1, 8, 1) }, false);
findState.change({ searchString: 'hello', wholeWord: true, searchScope: [new Range(7, 1, 8, 1)] }, false);
let findModel = new FindModelBoundToEditorModel(editor, findState);
assertFindState(
+42 -26
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { alert } from 'vs/base/browser/ui/aria/aria';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { asArray, isNonEmptyArray } from 'vs/base/common/arrays';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors';
import { URI } from 'vs/base/common/uri';
@@ -120,11 +120,12 @@ export abstract class FormattingConflicts {
}
}
export async function formatDocumentRangeWithSelectedProvider(
export async function formatDocumentRangesWithSelectedProvider(
accessor: ServicesAccessor,
editorOrModel: ITextModel | IActiveCodeEditor,
range: Range,
rangeOrRanges: Range | Range[],
mode: FormattingMode,
progress: IProgress<DocumentRangeFormattingEditProvider>,
token: CancellationToken
): Promise<void> {
@@ -133,15 +134,16 @@ export async function formatDocumentRangeWithSelectedProvider(
const provider = DocumentRangeFormattingEditProviderRegistry.ordered(model);
const selected = await FormattingConflicts.select(provider, model, mode);
if (selected) {
await instaService.invokeFunction(formatDocumentRangeWithProvider, selected, editorOrModel, range, token);
progress.report(selected);
await instaService.invokeFunction(formatDocumentRangesWithProvider, selected, editorOrModel, rangeOrRanges, token);
}
}
export async function formatDocumentRangeWithProvider(
export async function formatDocumentRangesWithProvider(
accessor: ServicesAccessor,
provider: DocumentRangeFormattingEditProvider,
editorOrModel: ITextModel | IActiveCodeEditor,
range: Range,
rangeOrRanges: Range | Range[],
token: CancellationToken
): Promise<boolean> {
const workerService = accessor.get(IEditorWorkerService);
@@ -156,39 +158,53 @@ export async function formatDocumentRangeWithProvider(
cts = new TextModelCancellationTokenSource(editorOrModel, token);
}
let edits: TextEdit[] | undefined;
try {
const rawEdits = await provider.provideDocumentRangeFormattingEdits(
model,
range,
model.getFormattingOptions(),
cts.token
);
edits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits);
if (cts.token.isCancellationRequested) {
return true;
// make sure that ranges don't overlap nor touch each other
let ranges: Range[] = [];
let len = 0;
for (let range of asArray(rangeOrRanges).sort(Range.compareRangesUsingStarts)) {
if (len > 0 && Range.areIntersectingOrTouching(ranges[len - 1], range)) {
ranges[len - 1] = Range.fromPositions(ranges[len - 1].getStartPosition(), range.getEndPosition());
} else {
len = ranges.push(range);
}
} finally {
cts.dispose();
}
if (!edits || edits.length === 0) {
const allEdits: TextEdit[] = [];
for (let range of ranges) {
try {
const rawEdits = await provider.provideDocumentRangeFormattingEdits(
model,
range,
model.getFormattingOptions(),
cts.token
);
const minEdits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits);
if (minEdits) {
allEdits.push(...minEdits);
}
if (cts.token.isCancellationRequested) {
return true;
}
} finally {
cts.dispose();
}
}
if (allEdits.length === 0) {
return false;
}
if (isCodeEditor(editorOrModel)) {
// use editor to apply edits
FormattingEdit.execute(editorOrModel, edits, true);
alertFormattingEdits(edits);
FormattingEdit.execute(editorOrModel, allEdits, true);
alertFormattingEdits(allEdits);
editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), ScrollType.Immediate);
} else {
// use model to apply edits
const [{ range }] = edits;
const [{ range }] = allEdits;
const initialSelection = new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
model.pushEditOperations([initialSelection], edits.map(edit => {
model.pushEditOperations([initialSelection], allEdits.map(edit => {
return {
text: edit.text,
range: Range.lift(edit.range),
+14 -5
View File
@@ -16,7 +16,7 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { DocumentRangeFormattingEditProviderRegistry, OnTypeFormattingEditProviderRegistry } from 'vs/editor/common/modes';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangeWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangesWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit';
import * as nls from 'vs/nls';
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
@@ -25,7 +25,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { onUnexpectedError } from 'vs/base/common/errors';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { Progress } from 'vs/platform/progress/common/progress';
import { Progress, IEditorProgressService } from 'vs/platform/progress/common/progress';
class FormatOnType implements IEditorContribution {
@@ -202,7 +202,7 @@ class FormatOnPaste implements IEditorContribution {
if (this.editor.getSelections().length > 1) {
return;
}
this._instantiationService.invokeFunction(formatDocumentRangeWithSelectedProvider, this.editor, range, FormattingMode.Silent, CancellationToken.None).catch(onUnexpectedError);
this._instantiationService.invokeFunction(formatDocumentRangesWithSelectedProvider, this.editor, range, FormattingMode.Silent, Progress.None, CancellationToken.None).catch(onUnexpectedError);
}
}
@@ -231,7 +231,11 @@ class FormatDocumentAction extends EditorAction {
async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
if (editor.hasModel()) {
const instaService = accessor.get(IInstantiationService);
await instaService.invokeFunction(formatDocumentWithSelectedProvider, editor, FormattingMode.Explicit, Progress.None, CancellationToken.None);
const progressService = accessor.get(IEditorProgressService);
await progressService.showWhile(
instaService.invokeFunction(formatDocumentWithSelectedProvider, editor, FormattingMode.Explicit, Progress.None, CancellationToken.None),
250
);
}
}
}
@@ -267,7 +271,12 @@ class FormatSelectionAction extends EditorAction {
if (range.isEmpty()) {
range = new Range(range.startLineNumber, 1, range.startLineNumber, model.getLineMaxColumn(range.startLineNumber));
}
await instaService.invokeFunction(formatDocumentRangeWithSelectedProvider, editor, range, FormattingMode.Explicit, CancellationToken.None);
const progressService = accessor.get(IEditorProgressService);
await progressService.showWhile(
instaService.invokeFunction(formatDocumentRangesWithSelectedProvider, editor, range, FormattingMode.Explicit, Progress.None, CancellationToken.None),
250
);
}
}
@@ -167,7 +167,7 @@ class MessageWidget {
let relatedResource = document.createElement('a');
dom.addClass(relatedResource, 'filename');
relatedResource.innerHTML = `${getBaseLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `;
relatedResource.innerText = `${getBaseLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `;
relatedResource.title = getPathLabel(related.resource, undefined);
this._relatedDiagnostics.set(relatedResource, related);
@@ -429,7 +429,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget {
if (this._model.isEmpty) {
this.setTitle('');
this._messageContainer.innerHTML = nls.localize('noResults', "No results");
this._messageContainer.innerText = nls.localize('noResults', "No results");
dom.show(this._messageContainer);
return Promise.resolve(undefined);
}
@@ -601,13 +601,15 @@ export class MultiCursorSelectionController extends Disposable implements IEdito
}
if (findState.searchScope) {
const state = findState.searchScope;
const states = findState.searchScope;
let inSelection: FindMatch[] | null = [];
for (let i = 0; i < matches.length; i++) {
if (matches[i].range.endLineNumber <= state.endLineNumber && matches[i].range.startLineNumber >= state.startLineNumber) {
inSelection.push(matches[i]);
}
}
matches.forEach((match) => {
states.forEach((state) => {
if (match.range.endLineNumber <= state.endLineNumber && match.range.startLineNumber >= state.startLineNumber) {
inSelection!.push(match);
}
});
});
matches = inSelection;
}
@@ -194,8 +194,8 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget {
dom.toggleClass(this.domNodes.element, 'multiple', multiple);
this.keyMultipleSignatures.set(multiple);
this.domNodes.signature.innerHTML = '';
this.domNodes.docs.innerHTML = '';
this.domNodes.signature.innerText = '';
this.domNodes.docs.innerText = '';
const signature = hints.signatures[hints.activeSignature];
if (!signature) {
+3 -4
View File
@@ -11,7 +11,6 @@ import { Action } from 'vs/base/common/actions';
import { Color } from 'vs/base/common/color';
import { Emitter } from 'vs/base/common/event';
import * as objects from 'vs/base/common/objects';
import * as strings from 'vs/base/common/strings';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
@@ -223,10 +222,10 @@ export abstract class PeekViewWidget extends ZoneWidget {
setTitle(primaryHeading: string, secondaryHeading?: string): void {
if (this._primaryHeading && this._secondaryHeading) {
this._primaryHeading.innerHTML = strings.escape(primaryHeading);
this._primaryHeading.innerText = primaryHeading;
this._primaryHeading.setAttribute('aria-label', primaryHeading);
if (secondaryHeading) {
this._secondaryHeading.innerHTML = strings.escape(secondaryHeading);
this._secondaryHeading.innerText = secondaryHeading;
} else {
dom.clearNode(this._secondaryHeading);
}
@@ -236,7 +235,7 @@ export abstract class PeekViewWidget extends ZoneWidget {
setMetaTitle(value: string): void {
if (this._metaHeading) {
if (value) {
this._metaHeading.innerHTML = strings.escape(value);
this._metaHeading.innerText = value;
dom.show(this._metaHeading);
} else {
dom.hide(this._metaHeading);
+219 -140
View File
@@ -8,7 +8,7 @@ import * as nls from 'vs/nls';
import { registerEditorContribution, registerModelAndPositionCommand, EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand } from 'vs/editor/browser/editorExtensions';
import * as arrays from 'vs/base/common/arrays';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { Disposable } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { Position, IPosition } from 'vs/editor/common/core/position';
@@ -16,7 +16,7 @@ import { ITextModel, IModelDeltaDecoration, TrackedRangeStickiness, IIdentifiedS
import { CancellationToken } from 'vs/base/common/cancellation';
import { IRange, Range } from 'vs/editor/common/core/range';
import { OnTypeRenameProviderRegistry } from 'vs/editor/common/modes';
import { first, createCancelablePromise, CancelablePromise, RunOnceScheduler } from 'vs/base/common/async';
import { first, createCancelablePromise, CancelablePromise, Delayer } from 'vs/base/common/async';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { ContextKeyExpr, RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
@@ -24,11 +24,12 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { URI } from 'vs/base/common/uri';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors';
import { isPromiseCanceledError, onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors';
import * as strings from 'vs/base/common/strings';
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { Color } from 'vs/base/common/color';
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
export const CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE = new RawContextKey<boolean>('onTypeRenameInputVisible', false);
@@ -50,14 +51,19 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr
private readonly _visibleContextKey: IContextKey<boolean>;
private _currentRequest: CancelablePromise<{
ranges: IRange[],
stopPattern?: RegExp
} | null | undefined> | null;
private _rangeUpdateTriggerPromise: Promise<any> | null;
private _rangeSyncTriggerPromise: Promise<any> | null;
private _currentRequest: CancelablePromise<any> | null;
private _currentRequestPosition: Position | null;
private _currentRequestModelVersion: number | null;
private _currentDecorations: string[]; // The one at index 0 is the reference one
private _stopPattern: RegExp;
private _languageWordPattern: RegExp | null;
private _currentWordPattern: RegExp | null;
private _ignoreChangeEvent: boolean;
private _updateMirrors: RunOnceScheduler;
private readonly _localToDispose = this._register(new DisposableStore());
constructor(
editor: ICodeEditor,
@@ -65,103 +71,117 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr
) {
super();
this._editor = editor;
this._enabled = this._editor.getOption(EditorOption.renameOnType);
this._enabled = false;
this._visibleContextKey = CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE.bindTo(contextKeyService);
this._currentRequest = null;
this._currentDecorations = [];
this._stopPattern = /^\s/;
this._languageWordPattern = null;
this._currentWordPattern = null;
this._ignoreChangeEvent = false;
this._updateMirrors = this._register(new RunOnceScheduler(() => this._doUpdateMirrors(), 0));
this._localToDispose = this._register(new DisposableStore());
this._register(this._editor.onDidChangeModel((e) => {
this.stopAll();
this.run();
}));
this._rangeUpdateTriggerPromise = null;
this._rangeSyncTriggerPromise = null;
this._register(this._editor.onDidChangeConfiguration((e) => {
this._currentRequest = null;
this._currentRequestPosition = null;
this._currentRequestModelVersion = null;
this._register(this._editor.onDidChangeModel(() => this.reinitialize()));
this._register(this._editor.onDidChangeConfiguration(e => {
if (e.hasChanged(EditorOption.renameOnType)) {
this._enabled = this._editor.getOption(EditorOption.renameOnType);
this.stopAll();
this.run();
this.reinitialize();
}
}));
this._register(OnTypeRenameProviderRegistry.onDidChange(() => this.reinitialize()));
this._register(this._editor.onDidChangeModelLanguage(() => this.reinitialize()));
this._register(this._editor.onDidChangeCursorPosition((e) => {
// no regions, run
if (this._currentDecorations.length === 0) {
this.run(e.position);
}
// has cached regions, don't run
if (!this._editor.hasModel()) {
return;
}
if (this._currentDecorations.length === 0) {
return;
}
const model = this._editor.getModel();
const currentRanges = this._currentDecorations.map(decId => model.getDecorationRange(decId)!);
// just moving cursor around, don't run again
if (Range.containsPosition(currentRanges[0], e.position)) {
return;
}
// moving cursor out of primary region, run
this.run(e.position);
}));
this._register(OnTypeRenameProviderRegistry.onDidChange(() => {
this.run();
}));
this._register(this._editor.onDidChangeModelContent((e) => {
if (this._ignoreChangeEvent) {
return;
}
if (!this._editor.hasModel()) {
return;
}
if (this._currentDecorations.length === 0) {
// nothing to do
return;
}
if (e.isUndoing || e.isRedoing) {
return;
}
if (e.changes[0] && this._stopPattern.test(e.changes[0].text)) {
this.stopAll();
return;
}
this._updateMirrors.schedule();
}));
this.reinitialize();
}
private _doUpdateMirrors(): void {
if (!this._editor.hasModel()) {
private reinitialize() {
const model = this._editor.getModel();
const isEnabled = model !== null && this._editor.getOption(EditorOption.renameOnType) && OnTypeRenameProviderRegistry.has(model);
if (isEnabled === this._enabled) {
return;
}
if (this._currentDecorations.length === 0) {
this._enabled = isEnabled;
this.clearRanges();
this._localToDispose.clear();
if (!isEnabled || model === null) {
return;
}
this._languageWordPattern = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageIdentifier().id);
this._localToDispose.add(model.onDidChangeLanguageConfiguration(() => {
this._languageWordPattern = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageIdentifier().id);
}));
const rangeUpdateScheduler = new Delayer(200);
const triggerRangeUpdate = () => {
this._rangeUpdateTriggerPromise = rangeUpdateScheduler.trigger(() => this.updateRanges());
};
const rangeSyncScheduler = new Delayer(0);
const triggerRangeSync = (decorations: string[]) => {
this._rangeSyncTriggerPromise = rangeSyncScheduler.trigger(() => this._syncRanges(decorations));
};
this._localToDispose.add(this._editor.onDidChangeCursorPosition((e) => {
triggerRangeUpdate();
}));
this._localToDispose.add(this._editor.onDidChangeModelContent((e) => {
if (!this._ignoreChangeEvent) {
if (this._currentDecorations.length > 0) {
const referenceRange = model.getDecorationRange(this._currentDecorations[0]);
if (referenceRange && e.changes.every(c => referenceRange.intersectRanges(c.range))) {
triggerRangeSync(this._currentDecorations);
return;
}
}
}
triggerRangeUpdate();
}));
this._localToDispose.add({
dispose: () => {
rangeUpdateScheduler.cancel();
rangeSyncScheduler.cancel();
}
});
this.updateRanges();
}
private _syncRanges(decorations: string[]): void {
// dalayed invocation, make sure we're still on
if (!this._editor.hasModel() || decorations !== this._currentDecorations || decorations.length === 0) {
// nothing to do
return;
}
const model = this._editor.getModel();
const currentRanges = this._currentDecorations.map(decId => model.getDecorationRange(decId)!);
const referenceRange = model.getDecorationRange(decorations[0]);
const referenceRange = currentRanges[0];
if (referenceRange.startLineNumber !== referenceRange.endLineNumber) {
return this.stopAll();
if (!referenceRange || referenceRange.startLineNumber !== referenceRange.endLineNumber) {
return this.clearRanges();
}
const referenceValue = model.getValueInRange(referenceRange);
if (this._stopPattern.test(referenceValue)) {
return this.stopAll();
if (this._currentWordPattern) {
const match = referenceValue.match(this._currentWordPattern);
const matchLength = match ? match[0].length : 0;
if (matchLength !== referenceValue.length) {
return this.clearRanges();
}
}
let edits: IIdentifiedSingleEditOperation[] = [];
for (let i = 1, len = currentRanges.length; i < len; i++) {
const mirrorRange = currentRanges[i];
for (let i = 1, len = decorations.length; i < len; i++) {
const mirrorRange = model.getDecorationRange(decorations[i]);
if (!mirrorRange) {
continue;
}
if (mirrorRange.startLineNumber !== mirrorRange.endLineNumber) {
edits.push({
range: mirrorRange,
@@ -207,72 +227,131 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr
}
public dispose(): void {
this.clearRanges();
super.dispose();
this.stopAll();
}
stopAll(): void {
public clearRanges(): void {
this._visibleContextKey.set(false);
this._currentDecorations = this._editor.deltaDecorations(this._currentDecorations, []);
}
async run(position: Position | null = this._editor.getPosition(), force = false): Promise<void> {
if (!position) {
return;
}
if (!this._enabled && !force) {
return;
}
if (!this._editor.hasModel()) {
return;
}
if (this._currentRequest) {
this._currentRequest.cancel();
this._currentRequest = null;
this._currentRequestPosition = null;
}
}
public get currentUpdateTriggerPromise(): Promise<any> {
return this._rangeUpdateTriggerPromise || Promise.resolve();
}
public get currentSyncTriggerPromise(): Promise<any> {
return this._rangeSyncTriggerPromise || Promise.resolve();
}
public async updateRanges(force = false): Promise<void> {
if (!this._editor.hasModel()) {
this.clearRanges();
return;
}
const position = this._editor.getPosition();
if (!this._enabled && !force || this._editor.getSelections().length > 1) {
// disabled or multicursor
this.clearRanges();
return;
}
const model = this._editor.getModel();
this._currentRequest = createCancelablePromise(token => getOnTypeRenameRanges(model, position, token));
try {
const response = await this._currentRequest;
let ranges: IRange[] = [];
if (response?.ranges) {
ranges = response.ranges;
const modelVersionId = model.getVersionId();
if (this._currentRequestPosition && this._currentRequestModelVersion === modelVersionId) {
if (position.equals(this._currentRequestPosition)) {
return; // same position
}
if (response?.stopPattern) {
this._stopPattern = response.stopPattern;
}
let foundReferenceRange = false;
for (let i = 0, len = ranges.length; i < len; i++) {
if (Range.containsPosition(ranges[i], position)) {
foundReferenceRange = true;
if (i !== 0) {
const referenceRange = ranges[i];
ranges.splice(i, 1);
ranges.unshift(referenceRange);
}
break;
if (this._currentDecorations && this._currentDecorations.length > 0) {
const range = model.getDecorationRange(this._currentDecorations[0]);
if (range && range.containsPosition(position)) {
return; // just moving inside the existing primary range
}
}
if (!foundReferenceRange) {
// Cannot do on type rename if the ranges are not where the cursor is...
this.stopAll();
return;
}
const decorations: IModelDeltaDecoration[] = ranges.map(range => ({ range: range, options: OnTypeRenameContribution.DECORATION }));
this._visibleContextKey.set(true);
this._currentDecorations = this._editor.deltaDecorations(this._currentDecorations, decorations);
} catch (err) {
onUnexpectedError(err);
this.stopAll();
}
this._currentRequestPosition = position;
this._currentRequestModelVersion = modelVersionId;
const request = createCancelablePromise(async token => {
try {
const response = await getOnTypeRenameRanges(model, position, token);
if (request !== this._currentRequest) {
return;
}
this._currentRequest = null;
if (modelVersionId !== model.getVersionId()) {
return;
}
let ranges: IRange[] = [];
if (response?.ranges) {
ranges = response.ranges;
}
this._currentWordPattern = response?.wordPattern || this._languageWordPattern;
let foundReferenceRange = false;
for (let i = 0, len = ranges.length; i < len; i++) {
if (Range.containsPosition(ranges[i], position)) {
foundReferenceRange = true;
if (i !== 0) {
const referenceRange = ranges[i];
ranges.splice(i, 1);
ranges.unshift(referenceRange);
}
break;
}
}
if (!foundReferenceRange) {
// Cannot do on type rename if the ranges are not where the cursor is...
this.clearRanges();
return;
}
const decorations: IModelDeltaDecoration[] = ranges.map(range => ({ range: range, options: OnTypeRenameContribution.DECORATION }));
this._visibleContextKey.set(true);
this._currentDecorations = this._editor.deltaDecorations(this._currentDecorations, decorations);
} catch (err) {
if (!isPromiseCanceledError(err)) {
onUnexpectedError(err);
}
if (this._currentRequest === request || !this._currentRequest) {
// stop if we are still the latest request
this.clearRanges();
}
}
});
this._currentRequest = request;
return request;
}
// private printDecorators(model: ITextModel) {
// return this._currentDecorations.map(d => {
// const range = model.getDecorationRange(d);
// if (range) {
// return this.printRange(range);
// }
// return 'invalid';
// }).join(',');
// }
// private printChanges(changes: IModelContentChange[]) {
// return changes.map(c => {
// return `${this.printRange(c.range)} - ${c.text}`;
// }
// ).join(',');
// }
// private printRange(range: IRange) {
// return `${range.startLineNumber},${range.startColumn}/${range.endLineNumber},${range.endColumn}`;
// }
}
export class OnTypeRenameAction extends EditorAction {
@@ -310,10 +389,10 @@ export class OnTypeRenameAction extends EditorAction {
return super.runCommand(accessor, args);
}
run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
const controller = OnTypeRenameContribution.get(editor);
if (controller) {
return Promise.resolve(controller.run(editor.getPosition(), true));
return Promise.resolve(controller.updateRanges(true));
}
return Promise.resolve();
}
@@ -323,7 +402,7 @@ const OnTypeRenameCommand = EditorCommand.bindToContribution<OnTypeRenameContrib
registerEditorCommand(new OnTypeRenameCommand({
id: 'cancelOnTypeRenameInput',
precondition: CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE,
handler: x => x.stopAll(),
handler: x => x.clearRanges(),
kbOpts: {
kbExpr: EditorContextKeys.editorTextFocus,
weight: KeybindingWeight.EditorContrib + 99,
@@ -335,7 +414,7 @@ registerEditorCommand(new OnTypeRenameCommand({
export function getOnTypeRenameRanges(model: ITextModel, position: Position, token: CancellationToken): Promise<{
ranges: IRange[],
stopPattern?: RegExp
wordPattern?: RegExp
} | undefined | null> {
const orderedByScore = OnTypeRenameProviderRegistry.ordered(model);
@@ -344,16 +423,16 @@ export function getOnTypeRenameRanges(model: ITextModel, position: Position, tok
// (good = none empty array)
return first<{
ranges: IRange[],
stopPattern?: RegExp
wordPattern?: RegExp
} | undefined>(orderedByScore.map(provider => () => {
return Promise.resolve(provider.provideOnTypeRenameRanges(model, position, token)).then((ranges) => {
if (!ranges) {
return Promise.resolve(provider.provideOnTypeRenameRanges(model, position, token)).then((res) => {
if (!res) {
return undefined;
}
return {
ranges,
stopPattern: provider.stopPattern
ranges: res.ranges,
wordPattern: res.wordPattern || provider.wordPattern
};
}, (err) => {
onUnexpectedExternalError(err);
@@ -6,19 +6,29 @@
import * as assert from 'assert';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { Handler } from 'vs/editor/common/editorCommon';
import * as modes from 'vs/editor/common/modes';
import { OnTypeRenameContribution } from 'vs/editor/contrib/rename/onTypeRename';
import { createTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands';
import { ITextModel } from 'vs/editor/common/model';
import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper';
const mockFile = URI.parse('test:somefile.ttt');
const mockFileSelector = { scheme: 'test' };
const timeout = 30;
interface TestEditor {
setPosition(pos: Position): Promise<any>;
setSelection(sel: IRange): Promise<any>;
trigger(source: string | null | undefined, handlerId: string, payload: any): Promise<any>;
undo(): void;
redo(): void;
}
suite('On type rename', () => {
const disposables = new DisposableStore();
@@ -45,26 +55,53 @@ suite('On type rename', () => {
function testCase(
name: string,
initialState: { text: string | string[], ranges: Range[], stopPattern?: RegExp },
operations: (editor: ITestCodeEditor, contrib: OnTypeRenameContribution) => Promise<void>,
initialState: { text: string | string[], responseWordPattern?: RegExp, providerWordPattern?: RegExp },
operations: (editor: TestEditor) => Promise<void>,
expectedEndText: string | string[]
) {
test(name, async () => {
disposables.add(modes.OnTypeRenameProviderRegistry.register(mockFileSelector, {
stopPattern: initialState.stopPattern || /^\s/,
provideOnTypeRenameRanges() {
return initialState.ranges;
wordPattern: initialState.providerWordPattern,
provideOnTypeRenameRanges(model: ITextModel, pos: IPosition) {
const wordAtPos = model.getWordAtPosition(pos);
if (wordAtPos) {
const matches = model.findMatches(wordAtPos.word, false, false, true, USUAL_WORD_SEPARATORS, false);
assert.ok(matches.length > 0);
return { ranges: matches.map(m => m.range), wordPattern: initialState.responseWordPattern };
}
return { ranges: [], wordPattern: initialState.responseWordPattern };
}
}));
const editor = createMockEditor(initialState.text);
editor.updateOptions({ renameOnType: true });
const ontypeRenameContribution = editor.registerAndInstantiateContribution(
OnTypeRenameContribution.ID,
OnTypeRenameContribution
);
await operations(editor, ontypeRenameContribution);
const testEditor: TestEditor = {
setPosition(pos: Position) {
editor.setPosition(pos);
return ontypeRenameContribution.currentUpdateTriggerPromise;
},
setSelection(sel: IRange) {
editor.setSelection(sel);
return ontypeRenameContribution.currentUpdateTriggerPromise;
},
trigger(source: string | null | undefined, handlerId: string, payload: any) {
editor.trigger(source, handlerId, payload);
return ontypeRenameContribution.currentSyncTriggerPromise;
},
undo() {
CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
},
redo() {
CoreEditingCommands.Redo.runEditorCommand(null, editor, null);
}
};
await operations(testEditor);
return new Promise((resolve) => {
setTimeout(() => {
@@ -80,349 +117,322 @@ suite('On type rename', () => {
}
const state = {
text: '<ooo></ooo>',
ranges: [
new Range(1, 2, 1, 5),
new Range(1, 8, 1, 11),
]
text: '<ooo></ooo>'
};
/**
* Simple insertion
*/
testCase('Simple insert - initial', state, async (editor, ontypeRenameContribution) => {
testCase('Simple insert - initial', state, async (editor) => {
const pos = new Position(1, 2);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<iooo></iooo>');
testCase('Simple insert - middle', state, async (editor, ontypeRenameContribution) => {
testCase('Simple insert - middle', state, async (editor) => {
const pos = new Position(1, 3);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<oioo></oioo>');
testCase('Simple insert - end', state, async (editor, ontypeRenameContribution) => {
testCase('Simple insert - end', state, async (editor) => {
const pos = new Position(1, 5);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<oooi></oooi>');
/**
* Simple insertion - end
*/
testCase('Simple insert end - initial', state, async (editor, ontypeRenameContribution) => {
testCase('Simple insert end - initial', state, async (editor) => {
const pos = new Position(1, 8);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<iooo></iooo>');
testCase('Simple insert end - middle', state, async (editor, ontypeRenameContribution) => {
testCase('Simple insert end - middle', state, async (editor) => {
const pos = new Position(1, 9);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<oioo></oioo>');
testCase('Simple insert end - end', state, async (editor, ontypeRenameContribution) => {
testCase('Simple insert end - end', state, async (editor) => {
const pos = new Position(1, 11);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<oooi></oooi>');
/**
* Boundary insertion
*/
testCase('Simple insert - out of boundary', state, async (editor, ontypeRenameContribution) => {
testCase('Simple insert - out of boundary', state, async (editor) => {
const pos = new Position(1, 1);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, 'i<ooo></ooo>');
testCase('Simple insert - out of boundary 2', state, async (editor, ontypeRenameContribution) => {
testCase('Simple insert - out of boundary 2', state, async (editor) => {
const pos = new Position(1, 6);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<ooo>i</ooo>');
testCase('Simple insert - out of boundary 3', state, async (editor, ontypeRenameContribution) => {
testCase('Simple insert - out of boundary 3', state, async (editor) => {
const pos = new Position(1, 7);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<ooo><i/ooo>');
testCase('Simple insert - out of boundary 4', state, async (editor, ontypeRenameContribution) => {
testCase('Simple insert - out of boundary 4', state, async (editor) => {
const pos = new Position(1, 12);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<ooo></ooo>i');
/**
* Insert + Move
*/
testCase('Continuous insert', state, async (editor, ontypeRenameContribution) => {
testCase('Continuous insert', state, async (editor) => {
const pos = new Position(1, 2);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<iiooo></iiooo>');
testCase('Insert - move - insert', state, async (editor, ontypeRenameContribution) => {
testCase('Insert - move - insert', state, async (editor) => {
const pos = new Position(1, 2);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
editor.setPosition(new Position(1, 4));
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(new Position(1, 4));
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<ioioo></ioioo>');
testCase('Insert - move - insert outside region', state, async (editor, ontypeRenameContribution) => {
testCase('Insert - move - insert outside region', state, async (editor) => {
const pos = new Position(1, 2);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
editor.setPosition(new Position(1, 7));
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(new Position(1, 7));
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<iooo>i</iooo>');
/**
* Selection insert
*/
testCase('Selection insert - simple', state, async (editor, ontypeRenameContribution) => {
testCase('Selection insert - simple', state, async (editor) => {
const pos = new Position(1, 2);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.setSelection(new Range(1, 2, 1, 3));
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.setSelection(new Range(1, 2, 1, 3));
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<ioo></ioo>');
testCase('Selection insert - whole', state, async (editor, ontypeRenameContribution) => {
testCase('Selection insert - whole', state, async (editor) => {
const pos = new Position(1, 2);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.setSelection(new Range(1, 2, 1, 5));
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.setSelection(new Range(1, 2, 1, 5));
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<i></i>');
testCase('Selection insert - across boundary', state, async (editor, ontypeRenameContribution) => {
testCase('Selection insert - across boundary', state, async (editor) => {
const pos = new Position(1, 2);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.setSelection(new Range(1, 1, 1, 3));
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.setSelection(new Range(1, 1, 1, 3));
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, 'ioo></oo>');
/**
* @todo
* Undefined behavior
*/
// testCase('Selection insert - across two boundary', state, async (editor, ontypeRenameContribution) => {
// testCase('Selection insert - across two boundary', state, async (editor) => {
// const pos = new Position(1, 2);
// editor.setPosition(pos);
// await ontypeRenameContribution.run(pos, true);
// editor.setSelection(new Range(1, 4, 1, 9));
// editor.trigger('keyboard', Handler.Type, { text: 'i' });
// await editor.setPosition(pos);
// await ontypeRenameContribution.updateLinkedUI(pos);
// await editor.setSelection(new Range(1, 4, 1, 9));
// await editor.trigger('keyboard', Handler.Type, { text: 'i' });
// }, '<ooioo>');
/**
* Break out behavior
*/
testCase('Breakout - type space', state, async (editor, ontypeRenameContribution) => {
testCase('Breakout - type space', state, async (editor) => {
const pos = new Position(1, 5);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: ' ' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: ' ' });
}, '<ooo ></ooo>');
testCase('Breakout - type space then undo', state, async (editor, ontypeRenameContribution) => {
testCase('Breakout - type space then undo', state, async (editor) => {
const pos = new Position(1, 5);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: ' ' });
CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: ' ' });
editor.undo();
}, '<ooo></ooo>');
testCase('Breakout - type space in middle', state, async (editor, ontypeRenameContribution) => {
testCase('Breakout - type space in middle', state, async (editor) => {
const pos = new Position(1, 4);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: ' ' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: ' ' });
}, '<oo o></ooo>');
testCase('Breakout - paste content starting with space', state, async (editor, ontypeRenameContribution) => {
testCase('Breakout - paste content starting with space', state, async (editor) => {
const pos = new Position(1, 5);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' });
}, '<ooo i="i"></ooo>');
testCase('Breakout - paste content starting with space then undo', state, async (editor, ontypeRenameContribution) => {
testCase('Breakout - paste content starting with space then undo', state, async (editor) => {
const pos = new Position(1, 5);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' });
CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' });
editor.undo();
}, '<ooo></ooo>');
testCase('Breakout - paste content starting with space in middle', state, async (editor, ontypeRenameContribution) => {
testCase('Breakout - paste content starting with space in middle', state, async (editor) => {
const pos = new Position(1, 4);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Paste, { text: ' i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Paste, { text: ' i' });
}, '<oo io></ooo>');
/**
* Break out with custom stopPattern
* Break out with custom provider wordPattern
*/
const state3 = {
...state,
stopPattern: /^s/
providerWordPattern: /[a-yA-Y]+/
};
testCase('Breakout with stop pattern - insert', state3, async (editor, ontypeRenameContribution) => {
testCase('Breakout with stop pattern - insert', state3, async (editor) => {
const pos = new Position(1, 2);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<iooo></iooo>');
testCase('Breakout with stop pattern - insert stop char', state3, async (editor, ontypeRenameContribution) => {
testCase('Breakout with stop pattern - insert stop char', state3, async (editor) => {
const pos = new Position(1, 2);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 's' });
}, '<sooo></ooo>');
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'z' });
}, '<zooo></ooo>');
testCase('Breakout with stop pattern - paste char', state3, async (editor, ontypeRenameContribution) => {
testCase('Breakout with stop pattern - paste char', state3, async (editor) => {
const pos = new Position(1, 2);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Paste, { text: 's' });
}, '<sooo></ooo>');
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Paste, { text: 'z' });
}, '<zooo></ooo>');
testCase('Breakout with stop pattern - paste string', state3, async (editor, ontypeRenameContribution) => {
testCase('Breakout with stop pattern - paste string', state3, async (editor) => {
const pos = new Position(1, 2);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Paste, { text: 'so' });
}, '<soooo></ooo>');
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Paste, { text: 'zo' });
}, '<zoooo></ooo>');
testCase('Breakout with stop pattern - insert at end', state3, async (editor, ontypeRenameContribution) => {
testCase('Breakout with stop pattern - insert at end', state3, async (editor) => {
const pos = new Position(1, 5);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 's' });
}, '<ooos></ooo>');
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'z' });
}, '<oooz></ooo>');
const state4 = {
...state,
providerWordPattern: /[a-yA-Y]+/,
responseWordPattern: /[a-eA-E]+/
};
testCase('Breakout with stop pattern - insert stop char, respos', state4, async (editor) => {
const pos = new Position(1, 2);
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<iooo></ooo>');
/**
* Delete
*/
testCase('Delete - left char', state, async (editor, ontypeRenameContribution) => {
testCase('Delete - left char', state, async (editor) => {
const pos = new Position(1, 5);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', 'deleteLeft', {});
await editor.setPosition(pos);
await editor.trigger('keyboard', 'deleteLeft', {});
}, '<oo></oo>');
testCase('Delete - left char then undo', state, async (editor, ontypeRenameContribution) => {
testCase('Delete - left char then undo', state, async (editor) => {
const pos = new Position(1, 5);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', 'deleteLeft', {});
CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
await editor.setPosition(pos);
await editor.trigger('keyboard', 'deleteLeft', {});
editor.undo();
}, '<ooo></ooo>');
testCase('Delete - left word', state, async (editor, ontypeRenameContribution) => {
testCase('Delete - left word', state, async (editor) => {
const pos = new Position(1, 5);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', 'deleteWordLeft', {});
await editor.setPosition(pos);
await editor.trigger('keyboard', 'deleteWordLeft', {});
}, '<></>');
testCase('Delete - left word then undo', state, async (editor, ontypeRenameContribution) => {
testCase('Delete - left word then undo', state, async (editor) => {
const pos = new Position(1, 5);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', 'deleteWordLeft', {});
CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
await editor.setPosition(pos);
await editor.trigger('keyboard', 'deleteWordLeft', {});
editor.undo();
editor.undo();
}, '<ooo></ooo>');
/**
* Todo: Fix test
*/
// testCase('Delete - left all', state, async (editor, ontypeRenameContribution) => {
// testCase('Delete - left all', state, async (editor) => {
// const pos = new Position(1, 3);
// editor.setPosition(pos);
// await ontypeRenameContribution.run(pos, true);
// editor.trigger('keyboard', 'deleteAllLeft', {});
// await editor.setPosition(pos);
// await ontypeRenameContribution.updateLinkedUI(pos);
// await editor.trigger('keyboard', 'deleteAllLeft', {});
// }, '></>');
/**
* Todo: Fix test
*/
// testCase('Delete - left all then undo', state, async (editor, ontypeRenameContribution) => {
// testCase('Delete - left all then undo', state, async (editor) => {
// const pos = new Position(1, 5);
// editor.setPosition(pos);
// await ontypeRenameContribution.run(pos, true);
// editor.trigger('keyboard', 'deleteAllLeft', {});
// CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
// await editor.setPosition(pos);
// await ontypeRenameContribution.updateLinkedUI(pos);
// await editor.trigger('keyboard', 'deleteAllLeft', {});
// editor.undo();
// }, '></ooo>');
testCase('Delete - left all then undo twice', state, async (editor, ontypeRenameContribution) => {
testCase('Delete - left all then undo twice', state, async (editor) => {
const pos = new Position(1, 5);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', 'deleteAllLeft', {});
CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
await editor.setPosition(pos);
await editor.trigger('keyboard', 'deleteAllLeft', {});
editor.undo();
editor.undo();
}, '<ooo></ooo>');
testCase('Delete - selection', state, async (editor, ontypeRenameContribution) => {
testCase('Delete - selection', state, async (editor) => {
const pos = new Position(1, 5);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.setSelection(new Range(1, 2, 1, 3));
editor.trigger('keyboard', 'deleteLeft', {});
await editor.setPosition(pos);
await editor.setSelection(new Range(1, 2, 1, 3));
await editor.trigger('keyboard', 'deleteLeft', {});
}, '<oo></oo>');
testCase('Delete - selection across boundary', state, async (editor, ontypeRenameContribution) => {
testCase('Delete - selection across boundary', state, async (editor) => {
const pos = new Position(1, 3);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.setSelection(new Range(1, 1, 1, 3));
editor.trigger('keyboard', 'deleteLeft', {});
await editor.setPosition(pos);
await editor.setSelection(new Range(1, 1, 1, 3));
await editor.trigger('keyboard', 'deleteLeft', {});
}, 'oo></oo>');
/**
* Undo / redo
*/
testCase('Undo/redo - simple undo', state, async (editor, ontypeRenameContribution) => {
testCase('Undo/redo - simple undo', state, async (editor) => {
const pos = new Position(1, 2);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
editor.undo();
editor.undo();
}, '<ooo></ooo>');
testCase('Undo/redo - simple undo/redo', state, async (editor, ontypeRenameContribution) => {
testCase('Undo/redo - simple undo/redo', state, async (editor) => {
const pos = new Position(1, 2);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
CoreEditingCommands.Undo.runEditorCommand(null, editor, null);
CoreEditingCommands.Redo.runEditorCommand(null, editor, null);
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
editor.undo();
editor.redo();
}, '<iooo></iooo>');
/**
@@ -432,18 +442,13 @@ suite('On type rename', () => {
text: [
'<ooo>',
'</ooo>'
],
ranges: [
new Range(1, 2, 1, 5),
new Range(2, 3, 2, 6),
]
};
testCase('Multiline insert', state2, async (editor, ontypeRenameContribution) => {
testCase('Multiline insert', state2, async (editor) => {
const pos = new Position(1, 2);
editor.setPosition(pos);
await ontypeRenameContribution.run(pos, true);
editor.trigger('keyboard', Handler.Type, { text: 'i' });
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, [
'<iooo>',
'</iooo>'
@@ -11,7 +11,7 @@ import { SnippetParser, Variable, VariableResolver } from 'vs/editor/contrib/sni
import { TextModel } from 'vs/editor/common/model/textModel';
import { Workspace, toWorkspaceFolders, IWorkspace, IWorkspaceContextService, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { ILabelService } from 'vs/platform/label/common/label';
import { mock } from 'vs/editor/contrib/suggest/test/suggestModel.test';
import { mock } from 'vs/base/test/common/mock';
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
suite('Snippet Variables Resolver', function () {
@@ -229,7 +229,7 @@ export class SuggestModel implements IDisposable {
if (supports) {
// keep existing items that where not computed by the
// supports/providers that want to trigger now
const items: CompletionItem[] | undefined = this._completionModel ? this._completionModel.adopt(supports) : undefined;
const items = this._completionModel?.adopt(supports);
this.trigger({ auto: true, shy: false, triggerCharacter: lastChar }, Boolean(this._completionModel), supports, items);
}
};
@@ -556,6 +556,12 @@ export class SuggestModel implements IDisposable {
return;
}
if (ctx.leadingWord.word.length !== 0 && ctx.leadingWord.startColumn > this._context.leadingWord.startColumn) {
// started a new word while IntelliSense shows -> retrigger
this.trigger({ auto: this._context.auto, shy: false }, true);
return;
}
if (ctx.column > this._context.column && this._completionModel.incomplete.size > 0 && ctx.leadingWord.word.length !== 0) {
// typed -> moved cursor RIGHT & incomple model & still on a word -> retrigger
const { incomplete } = this._completionModel;
@@ -373,7 +373,7 @@ class SuggestionDetails {
this.docs.textContent = documentation;
} else {
this.docs.classList.add('markdown-docs');
this.docs.innerHTML = '';
this.docs.innerText = '';
const renderedContents = this.markdownRenderer.render(documentation);
this.renderDisposeable = renderedContents;
this.docs.appendChild(renderedContents.element);
@@ -17,7 +17,7 @@ import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { mock } from 'vs/editor/contrib/suggest/test/suggestModel.test';
import { mock } from 'vs/base/test/common/mock';
import { Selection } from 'vs/editor/common/core/selection';
import { CompletionProviderRegistry, CompletionItemKind, CompletionItemInsertTextRule } from 'vs/editor/common/modes';
import { Event } from 'vs/base/common/event';
@@ -34,14 +34,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
export interface Ctor<T> {
new(): T;
}
export function mock<T>(): Ctor<T> {
return function () { } as any;
}
import { mock } from 'vs/base/test/common/mock';
function createMockEditor(model: TextModel): ITestCodeEditor {
@@ -798,4 +791,68 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
});
});
test('Trigger (full) completions when (incomplete) completions are already active #99504', function () {
let countA = 0;
let countB = 0;
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos) {
countA += 1;
return {
incomplete: false, // doesn't matter if incomplete or not
suggestions: [{
kind: CompletionItemKind.Class,
label: 'Z aaa',
insertText: 'Z aaa',
range: new Range(1, 1, pos.lineNumber, pos.column)
}],
};
}
}));
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos) {
countB += 1;
return {
incomplete: false,
suggestions: [{
kind: CompletionItemKind.Folder,
label: 'aaa',
insertText: 'aaa',
range: getDefaultSuggestRange(doc, pos)
}],
};
},
}));
return withOracle(async (model, editor) => {
await assertEvent(model.onDidSuggest, () => {
editor.setValue('');
editor.setSelection(new Selection(1, 1, 1, 1));
editor.trigger('keyboard', Handler.Type, { text: 'Z' });
}, event => {
assert.equal(event.auto, true);
assert.equal(event.completionModel.items.length, 1);
assert.equal(event.completionModel.items[0].textLabel, 'Z aaa');
});
await assertEvent(model.onDidSuggest, () => {
// started another word: Z a|
// item should be: Z aaa, aaa
editor.trigger('keyboard', Handler.Type, { text: ' a' });
}, event => {
assert.equal(event.auto, true);
assert.equal(event.completionModel.items.length, 2);
assert.equal(event.completionModel.items[0].textLabel, 'Z aaa');
assert.equal(event.completionModel.items[1].textLabel, 'aaa');
assert.equal(countA, 2); // should we keep the suggestions from the "active" provider?
assert.equal(countB, 2);
});
});
});
});
@@ -5,7 +5,7 @@
import * as assert from 'assert';
import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker';
import { mock } from 'vs/editor/contrib/suggest/test/suggestModel.test';
import { mock } from 'vs/base/test/common/mock';
import { EditorWorkerHost, EditorWorkerServiceImpl } from 'vs/editor/common/services/editorWorkerServiceImpl';
import { IModelService } from 'vs/editor/common/services/modelService';
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
@@ -81,11 +81,16 @@ 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 () {
disposables.clear();
});
function createSuggestItem(label: string, overwriteBefore: number, position: IPosition): CompletionItem {
const suggestion: modes.CompletionItem = {
label,
@@ -265,8 +265,8 @@ class InspectTokensWidget extends Disposable implements IContentWidget {
case StandardTokenType.Comment: return 'Comment';
case StandardTokenType.String: return 'String';
case StandardTokenType.RegEx: return 'RegEx';
default: return '??';
}
return '??';
}
private _fontStyleToString(fontStyle: FontStyle): string {
@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as browser from 'vs/base/browser/browser';
import { createFastDomNode } from 'vs/base/browser/fastDomNode';
import { ITextAreaInputHost, TextAreaInput } from 'vs/editor/browser/controller/textAreaInput';
import { ISimpleModel, PagedScreenReaderStrategy, TextAreaState } from 'vs/editor/browser/controller/textAreaState';
@@ -74,7 +73,7 @@ function doCreateTest(description: string, inputStr: string, expectedStr: string
container.appendChild(title);
let startBtn = document.createElement('button');
startBtn.innerHTML = 'Start';
startBtn.innerText = 'Start';
container.appendChild(startBtn);
@@ -96,12 +95,6 @@ function doCreateTest(description: string, inputStr: string, expectedStr: string
};
},
getScreenReaderContent: (currentState: TextAreaState): TextAreaState => {
if (browser.isIPad) {
// Do not place anything in the textarea for the iPad
return TextAreaState.EMPTY;
}
const selection = new Range(1, 1 + cursorOffset, 1, 1 + cursorOffset + cursorLength);
return PagedScreenReaderStrategy.fromEditorSelection(currentState, model, selection, 10, true);
@@ -141,10 +134,10 @@ function doCreateTest(description: string, inputStr: string, expectedStr: string
let expected = 'some ' + expectedStr + ' text';
if (text === expected) {
check.innerHTML = '[GOOD]';
check.innerText = '[GOOD]';
check.className = 'check good';
} else {
check.innerHTML = '[BAD]';
check.innerText = '[BAD]';
check.className = 'check bad';
}
check.innerHTML += expected;
+11 -4
View File
@@ -1761,7 +1761,7 @@ declare namespace monaco.editor {
/**
* Search the model.
* @param searchString The string used to search. If it is a regular expression, set `isRegex` to true.
* @param searchScope Limit the searching to only search inside this range.
* @param searchScope Limit the searching to only search inside these ranges.
* @param isRegex Used to indicate that `searchString` is a regular expression.
* @param matchCase Force the matching to match lower/upper case exactly.
* @param wordSeparators Force the matching to match entire words only. Pass null otherwise.
@@ -1769,7 +1769,7 @@ declare namespace monaco.editor {
* @param limitResultCount Limit the number of results
* @return The ranges where the matches are. It is empty if no matches have been found.
*/
findMatches(searchString: string, searchScope: IRange, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount?: number): FindMatch[];
findMatches(searchString: string, searchScope: IRange | IRange[], isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount?: number): FindMatch[];
/**
* Search the model for the next match. Loops to the beginning of the model if needed.
* @param searchString The string used to search. If it is a regular expression, set `isRegex` to true.
@@ -3290,6 +3290,10 @@ declare namespace monaco.editor {
* Configuration options for editor find widget
*/
export interface IEditorFindOptions {
/**
* Controls whether the cursor should move to find matches while typing.
*/
cursorMoveOnType?: boolean;
/**
* Controls if we seed search string in the Find Widget with editor selection.
*/
@@ -5793,11 +5797,14 @@ declare namespace monaco.languages {
* the live-rename feature.
*/
export interface OnTypeRenameProvider {
stopPattern?: RegExp;
wordPattern?: RegExp;
/**
* Provide a list of ranges that can be live-renamed together.
*/
provideOnTypeRenameRanges(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult<IRange[]>;
provideOnTypeRenameRanges(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult<{
ranges: IRange[];
wordPattern?: RegExp;
}>;
}
/**
@@ -304,7 +304,7 @@ export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem {
}
}
super(action, Array.isArray(action.actions) ? action.actions : action.actions(), _contextMenuService, { classNames });
super(action, action.actions, _contextMenuService, { classNames });
}
}
@@ -120,7 +120,9 @@ export class MenuId {
static readonly CommentTitle = new MenuId('CommentTitle');
static readonly CommentActions = new MenuId('CommentActions');
static readonly NotebookCellTitle = new MenuId('NotebookCellTitle');
static readonly NotebookCellInsert = new MenuId('NotebookCellInsert');
static readonly NotebookCellBetween = new MenuId('NotebookCellBetween');
static readonly NotebookCellListTop = new MenuId('NotebookCellTop');
static readonly BulkEditTitle = new MenuId('BulkEditTitle');
static readonly BulkEditContext = new MenuId('BulkEditContext');
static readonly ObjectExplorerItemContext = new MenuId('ObjectExplorerItemContext'); // {{SQL CARBON EDIT}}
@@ -72,6 +72,7 @@ export class ContextMenuHandler {
this.block.style.top = '0';
this.block.style.width = '100%';
this.block.style.height = '100%';
this.block.style.zIndex = '-1';
domEvent(this.block, EventType.MOUSE_DOWN)((e: MouseEvent) => e.stopPropagation());
}
@@ -5,15 +5,16 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const ICredentialsService = createDecorator<ICredentialsService>('ICredentialsService');
export interface ICredentialsService {
readonly _serviceBrand: undefined;
export interface ICredentialsProvider {
getPassword(service: string, account: string): Promise<string | null>;
setPassword(service: string, account: string, password: string): Promise<void>;
deletePassword(service: string, account: string): Promise<boolean>;
findPassword(service: string): Promise<string | null>;
findCredentials(service: string): Promise<Array<{ account: string, password: string }>>;
}
export const ICredentialsService = createDecorator<ICredentialsService>('ICredentialsService');
export interface ICredentialsService extends ICredentialsProvider {
readonly _serviceBrand: undefined;
}
+1 -2
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, CrashReporterStartOptions, MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes';
import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes';
import { IOpenedWindow, IWindowOpenable, IOpenEmptyWindowOptions, IOpenWindowOptions } from 'vs/platform/windows/common/windows';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
@@ -98,7 +98,6 @@ export interface ICommonElectronService {
// Development
openDevTools(options?: OpenDevToolsOptions): Promise<void>;
toggleDevTools(): Promise<void>;
startCrashReporter(options: CrashReporterStartOptions): Promise<void>;
sendInputEvent(event: MouseInputEvent): Promise<void>;
// Connectivity
@@ -5,7 +5,7 @@
import { Event } from 'vs/base/common/event';
import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, CrashReporterStartOptions, crashReporter, Menu, BrowserWindow, app, clipboard, powerMonitor } from 'electron';
import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor } from 'electron';
import { OpenContext } from 'vs/platform/windows/node/window';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows';
@@ -20,7 +20,6 @@ import { dirExists } from 'vs/base/node/pfs';
import { URI } from 'vs/base/common/uri';
import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes';
import { totalmem } from 'os';
@@ -38,8 +37,7 @@ export class ElectronMainService implements IElectronMainService {
@IDialogMainService private readonly dialogMainService: IDialogMainService,
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
@IEnvironmentService private readonly environmentService: INativeEnvironmentService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@ILogService private readonly logService: ILogService
@ITelemetryService private readonly telemetryService: ITelemetryService
) {
}
@@ -479,12 +477,6 @@ export class ElectronMainService implements IElectronMainService {
}
}
async startCrashReporter(windowId: number | undefined, options: CrashReporterStartOptions): Promise<void> {
this.logService.trace('ElectronMainService#crashReporter', JSON.stringify(options));
crashReporter.start(options);
}
async sendInputEvent(windowId: number | undefined, event: MouseInputEvent): Promise<void> {
const window = this.windowById(windowId);
if (window && (event.type === 'mouseDown' || event.type === 'mouseUp')) {
+2
View File
@@ -64,6 +64,7 @@ export interface ParsedArgs {
'disable-updates'?: boolean;
'disable-crash-reporter'?: boolean;
'crash-reporter-directory'?: string;
'crash-reporter-id'?: string;
'skip-add-to-recently-opened'?: boolean;
'max-memory'?: string;
'file-write'?: boolean;
@@ -191,6 +192,7 @@ export const OPTIONS: OptionDescriptions<Required<ParsedArgs>> = {
'disable-updates': { type: 'boolean' },
'disable-crash-reporter': { type: 'boolean' },
'crash-reporter-directory': { type: 'string' },
'crash-reporter-id': { type: 'string' },
'disable-user-env-probe': { type: 'boolean' },
'skip-add-to-recently-opened': { type: 'boolean' },
'unity-launch': { type: 'boolean' },
@@ -256,7 +256,7 @@ export class EnvironmentService implements INativeEnvironmentService {
get serviceMachineIdResource(): URI { return resources.joinPath(URI.file(this.userDataPath), 'machineid'); }
get disableUpdates(): boolean { return !!this._args['disable-updates']; }
get disableCrashReporter(): boolean { return !!this._args['disable-crash-reporter']; }
get crashReporterId(): string | undefined { return this._args['crash-reporter-id']; }
get crashReporterDirectory(): string | undefined { return this._args['crash-reporter-directory']; }
get driverHandle(): string | undefined { return this._args['driver']; }
@@ -70,14 +70,18 @@ export class LaunchMainService implements ILaunchMainService {
@IConfigurationService private readonly configurationService: IConfigurationService
) { }
start(args: ParsedArgs, userEnv: IProcessEnvironment): Promise<void> {
async start(args: ParsedArgs, userEnv: IProcessEnvironment): Promise<void> {
this.logService.trace('Received data from other instance: ', args, userEnv);
const urlsToOpen = parseOpenUrl(args);
// Since we now start to open a window, make sure the app has focus.
// Focussing a window will not ensure that the application itself
// has focus, so we use the `steal: true` hint to force focus.
app.focus({ steal: true });
// Check early for open-url which is handled in URL service
const urlsToOpen = parseOpenUrl(args);
if (urlsToOpen.length) {
let whenWindowReady: Promise<any> = Promise.resolve<any>(null);
let whenWindowReady: Promise<unknown> = Promise.resolve();
// Create a window if there is none
if (this.windowsMainService.getWindowCount() === 0) {
@@ -91,12 +95,12 @@ export class LaunchMainService implements ILaunchMainService {
this.urlService.open(url);
}
});
return Promise.resolve(undefined);
}
// Otherwise handle in windows service
return this.startOpenWindow(args, userEnv);
else {
return this.startOpenWindow(args, userEnv);
}
}
private startOpenWindow(args: ParsedArgs, userEnv: IProcessEnvironment): Promise<void> {
+1 -1
View File
@@ -21,7 +21,7 @@ if (isWeb) {
if (Object.keys(product).length === 0) {
Object.assign(product, {
version: '1.17.0-dev',
vscodeVersion: '1.48.0-dev',
vscodeVersion: '1.49.0-dev',
nameLong: 'Azure Data Studio Web Dev',
nameShort: 'Azure Data Studio Web Dev',
urlProtocol: 'azuredatastudio-oss',
+25 -3
View File
@@ -56,7 +56,13 @@ export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address:
};
}
export function isLocalhost(host: string): boolean {
return host === 'localhost' || host === '127.0.0.1';
}
function getOtherLocalhost(host: string): string | undefined {
return (host === 'localhost') ? '127.0.0.1' : ((host === '127.0.0.1') ? 'localhost' : undefined);
}
export abstract class AbstractTunnelService implements ITunnelService {
declare readonly _serviceBrand: undefined;
@@ -107,7 +113,7 @@ export abstract class AbstractTunnelService implements ITunnelService {
return undefined;
}
if (!remoteHost || (remoteHost === '127.0.0.1')) {
if (!remoteHost) {
remoteHost = 'localhost';
}
@@ -174,13 +180,29 @@ export abstract class AbstractTunnelService implements ITunnelService {
this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel });
}
protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise<RemoteTunnel> } | undefined {
const otherLocalhost = getOtherLocalhost(remoteHost);
let portMap: Map<number, { refcount: number, readonly value: Promise<RemoteTunnel> }> | undefined;
if (otherLocalhost) {
const firstMap = this._tunnels.get(remoteHost);
const secondMap = this._tunnels.get(otherLocalhost);
if (firstMap && secondMap) {
portMap = new Map([...Array.from(firstMap.entries()), ...Array.from(secondMap.entries())]);
} else {
portMap = firstMap ?? secondMap;
}
} else {
portMap = this._tunnels.get(remoteHost);
}
return portMap ? portMap.get(remotePort) : undefined;
}
protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
}
export class TunnelService extends AbstractTunnelService {
protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise<RemoteTunnel> | undefined {
const portMap = this._tunnels.get(remoteHost);
const existing = portMap ? portMap.get(remotePort) : undefined;
const existing = this.getTunnelFromMap(remoteHost, remotePort);
if (existing) {
++existing.refcount;
return existing.value;
+2 -3
View File
@@ -86,7 +86,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
this.tunnelLocalPort = address.port;
await this._barrier.wait();
this.localAddress = 'localhost:' + address.port;
this.localAddress = `${this.tunnelRemoteHost === '127.0.0.1' ? '127.0.0.1' : 'localhost'}:${address.port}`;
return this;
}
@@ -132,8 +132,7 @@ export class TunnelService extends AbstractTunnelService {
}
protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined {
const portMap = this._tunnels.get(remoteHost);
const existing = portMap ? portMap.get(remotePort) : undefined;
const existing = this.getTunnelFromMap(remoteHost, remotePort);
if (existing) {
++existing.refcount;
return existing.value;
@@ -20,8 +20,9 @@ export namespace SeverityIcon {
return Codicon.warning.classNames;
case Severity.Error:
return Codicon.error.classNames;
default:
return '';
}
return '';
}
}
@@ -5,7 +5,7 @@
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage';
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage, IS_NEW_KEY } from 'vs/platform/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { IFileService, FileChangeType } from 'vs/platform/files/common/files';
@@ -20,8 +20,6 @@ export class BrowserStorageService extends Disposable implements IStorageService
declare readonly _serviceBrand: undefined;
private static readonly WORKSPACE_IS_NEW_KEY = '__$__isNewStorageMarker';
private readonly _onDidChangeStorage = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
readonly onDidChangeStorage = this._onDidChangeStorage.event;
@@ -82,12 +80,20 @@ export class BrowserStorageService extends Disposable implements IStorageService
this.globalStorage.init()
]);
// Check to see if this is the first time we are "opening" this workspace
const firstOpen = this.workspaceStorage.getBoolean(BrowserStorageService.WORKSPACE_IS_NEW_KEY);
// Check to see if this is the first time we are "opening" the application
const firstOpen = this.globalStorage.getBoolean(IS_NEW_KEY);
if (firstOpen === undefined) {
this.workspaceStorage.set(BrowserStorageService.WORKSPACE_IS_NEW_KEY, true);
this.globalStorage.set(IS_NEW_KEY, true);
} else if (firstOpen) {
this.workspaceStorage.set(BrowserStorageService.WORKSPACE_IS_NEW_KEY, false);
this.globalStorage.set(IS_NEW_KEY, false);
}
// Check to see if this is the first time we are "opening" this workspace
const firstWorkspaceOpen = this.workspaceStorage.getBoolean(IS_NEW_KEY);
if (firstWorkspaceOpen === undefined) {
this.workspaceStorage.set(IS_NEW_KEY, true);
} else if (firstWorkspaceOpen) {
this.workspaceStorage.set(IS_NEW_KEY, false);
}
// In the browser we do not have support for long running unload sequences. As such,
@@ -189,8 +195,8 @@ export class BrowserStorageService extends Disposable implements IStorageService
this.dispose();
}
isNew(scope: StorageScope.WORKSPACE): boolean {
return this.getBoolean(BrowserStorageService.WORKSPACE_IS_NEW_KEY, scope) === true;
isNew(scope: StorageScope): boolean {
return this.getBoolean(IS_NEW_KEY, scope) === true;
}
dispose(): void {
+4 -3
View File
@@ -9,6 +9,8 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
export const IS_NEW_KEY = '__$__isNewStorageMarker';
export const IStorageService = createDecorator<IStorageService>('storageService');
export enum WillSaveStateReason {
@@ -104,12 +106,11 @@ export interface IStorageService {
migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void>;
/**
* Wether the storage for the given scope was created during this session or
* Whether the storage for the given scope was created during this session or
* existed before.
*
* Note: currently only implemented for `WORKSPACE` scope.
*/
isNew(scope: StorageScope.WORKSPACE): boolean;
isNew(scope: StorageScope): boolean;
/**
* Allows to flush state, e.g. in cases where a shutdown is

Some files were not shown because too many files have changed in this diff Show More