Merge from vscode ad407028575a77ea387eb7cc219b323dc017b686

This commit is contained in:
ADS Merger
2020-08-22 06:06:52 +00:00
committed by Anthony Dresser
parent 404260b8a0
commit 4ad73d381c
480 changed files with 14360 additions and 14122 deletions

View File

@@ -124,12 +124,12 @@ class DomCharWidthReader {
private static _render(testElement: HTMLElement, request: CharWidthRequest): void {
if (request.chr === ' ') {
let htmlString = ' ';
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

View File

@@ -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;
}
}

View File

@@ -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

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;
}

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 {

View File

@@ -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;

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);

View File

@@ -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;

View File

@@ -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')

View File

@@ -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 {

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.

View File

@@ -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 {

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 {

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; }>;
}
/**

View File

@@ -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 {
}
}
});

View File

@@ -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) {

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;
}
}

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);

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

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;

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 {

View File

@@ -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)]);
});
});
});

View File

@@ -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(

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),

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
);
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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) {

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);

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);

View File

@@ -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>'

View File

@@ -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 () {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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';

View File

@@ -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);
});
});
});
});

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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;