mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-08 01:28:26 -05:00
Merge from vscode 6e530127a1bb8ffbd1bfb77dc680c321dc0d71f5 (#6844)
This commit is contained in:
@@ -14,16 +14,25 @@ import { IOpenerService, IOpener } from 'vs/platform/opener/common/opener';
|
||||
import { equalsIgnoreCase } from 'vs/base/common/strings';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { LinkedList } from 'vs/base/common/linkedList';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IProductService } from 'vs/platform/product/common/product';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export class OpenerService implements IOpenerService {
|
||||
|
||||
_serviceBrand: any;
|
||||
_serviceBrand!: ServiceIdentifier<any>;
|
||||
|
||||
private readonly _opener = new LinkedList<IOpener>();
|
||||
|
||||
constructor(
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IDialogService private readonly _dialogService: IDialogService,
|
||||
@IProductService private readonly _productService: IProductService
|
||||
) {
|
||||
//
|
||||
}
|
||||
@@ -33,7 +42,7 @@ export class OpenerService implements IOpenerService {
|
||||
return { dispose: remove };
|
||||
}
|
||||
|
||||
async open(resource: URI, options?: { openToSide?: boolean }): Promise<boolean> {
|
||||
async open(resource: URI, options?: { openToSide?: boolean, openExternal?: boolean }): Promise<boolean> {
|
||||
// no scheme ?!?
|
||||
if (!resource.scheme) {
|
||||
return Promise.resolve(false);
|
||||
@@ -49,14 +58,58 @@ export class OpenerService implements IOpenerService {
|
||||
return this._doOpen(resource, options);
|
||||
}
|
||||
|
||||
private _doOpen(resource: URI, options?: { openToSide?: boolean }): Promise<boolean> {
|
||||
private _doOpen(resource: URI, options?: { openToSide?: boolean, openExternal?: boolean }): Promise<boolean> {
|
||||
|
||||
const { scheme, path, query, fragment } = resource;
|
||||
const { scheme, authority, path, query, fragment } = resource;
|
||||
|
||||
if (equalsIgnoreCase(scheme, Schemas.http) || equalsIgnoreCase(scheme, Schemas.https) || equalsIgnoreCase(scheme, Schemas.mailto)) {
|
||||
// open http or default mail application
|
||||
return this.openExternal(resource);
|
||||
if (equalsIgnoreCase(scheme, Schemas.mailto) || (options && options.openExternal)) {
|
||||
// open default mail application
|
||||
return this._doOpenExternal(resource);
|
||||
}
|
||||
|
||||
if (equalsIgnoreCase(scheme, Schemas.http) || equalsIgnoreCase(scheme, Schemas.https)) {
|
||||
let trustedDomains: string[] = ['https://code.visualstudio.com'];
|
||||
try {
|
||||
const trustedDomainsSrc = this._storageService.get('http.trustedDomains', StorageScope.GLOBAL);
|
||||
if (trustedDomainsSrc) {
|
||||
trustedDomains = JSON.parse(trustedDomainsSrc);
|
||||
}
|
||||
} catch (err) { }
|
||||
|
||||
const domainToOpen = `${scheme}://${authority}`;
|
||||
|
||||
if (isDomainTrusted(domainToOpen, trustedDomains)) {
|
||||
return this._doOpenExternal(resource);
|
||||
} else {
|
||||
return this._dialogService.show(
|
||||
Severity.Info,
|
||||
localize(
|
||||
'openExternalLinkAt',
|
||||
'Do you want {0} to open the external website?\n{1}',
|
||||
this._productService.nameShort,
|
||||
resource.toString(true)
|
||||
),
|
||||
[
|
||||
localize('openLink', 'Open Link'),
|
||||
localize('cancel', 'Cancel'),
|
||||
localize('configureTrustedDomains', 'Configure Trusted Domains')
|
||||
],
|
||||
{
|
||||
cancelId: 1
|
||||
}).then((choice) => {
|
||||
if (choice === 0) {
|
||||
return this._doOpenExternal(resource);
|
||||
} else if (choice === 2) {
|
||||
return this._commandService.executeCommand('workbench.action.configureTrustedDomains', domainToOpen).then((pickedDomains: string[]) => {
|
||||
if (pickedDomains.indexOf(domainToOpen) !== -1) {
|
||||
return this._doOpenExternal(resource);
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
}
|
||||
} else if (equalsIgnoreCase(scheme, Schemas.command)) {
|
||||
// run command or bail out if command isn't known
|
||||
if (!CommandsRegistry.getCommand(path)) {
|
||||
@@ -100,9 +153,27 @@ export class OpenerService implements IOpenerService {
|
||||
}
|
||||
}
|
||||
|
||||
openExternal(resource: URI): Promise<boolean> {
|
||||
private _doOpenExternal(resource: URI): Promise<boolean> {
|
||||
dom.windowOpenNoOpener(encodeURI(resource.toString(true)));
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a domain like https://www.microsoft.com matches
|
||||
* the list of trusted domains.
|
||||
*/
|
||||
function isDomainTrusted(domain: string, trustedDomains: string[]) {
|
||||
for (let i = 0; i < trustedDomains.length; i++) {
|
||||
if (trustedDomains[i] === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trustedDomains[i] === domain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,14 @@ export class CursorModelState {
|
||||
|
||||
class AutoClosedAction {
|
||||
|
||||
public static getAllAutoClosedCharacters(autoClosedActions: AutoClosedAction[]): Range[] {
|
||||
let autoClosedCharacters: Range[] = [];
|
||||
for (const autoClosedAction of autoClosedActions) {
|
||||
autoClosedCharacters = autoClosedCharacters.concat(autoClosedAction.getAutoClosedCharactersRanges());
|
||||
}
|
||||
return autoClosedCharacters;
|
||||
}
|
||||
|
||||
private readonly _model: ITextModel;
|
||||
|
||||
private _autoClosedCharactersDecorations: string[];
|
||||
@@ -593,11 +601,12 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
|
||||
}
|
||||
const closeChar = m[1];
|
||||
|
||||
const openChar = this.context.config.autoClosingPairsClose[closeChar];
|
||||
if (!openChar) {
|
||||
const autoClosingPairsCandidates = this.context.config.autoClosingPairsClose2.get(closeChar);
|
||||
if (!autoClosingPairsCandidates || autoClosingPairsCandidates.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const openChar = autoClosingPairsCandidates[0].open;
|
||||
const closeCharIndex = edit.text.length - m[2].length - 1;
|
||||
const openCharIndex = edit.text.lastIndexOf(openChar, closeCharIndex - 1);
|
||||
if (openCharIndex === -1) {
|
||||
@@ -738,7 +747,8 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
|
||||
private _interpretCompositionEnd(source: string) {
|
||||
if (!this._isDoingComposition && source === 'keyboard') {
|
||||
// composition finishes, let's check if we need to auto complete if necessary.
|
||||
this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections()));
|
||||
const autoClosedCharacters = AutoClosedAction.getAllAutoClosedCharacters(this._autoClosedActions);
|
||||
this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), autoClosedCharacters));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,14 +766,8 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
|
||||
chr = text.charAt(i);
|
||||
}
|
||||
|
||||
let autoClosedCharacters: Range[] = [];
|
||||
if (this._autoClosedActions.length > 0) {
|
||||
for (let i = 0, len = this._autoClosedActions.length; i < len; i++) {
|
||||
autoClosedCharacters = autoClosedCharacters.concat(this._autoClosedActions[i].getAutoClosedCharactersRanges());
|
||||
}
|
||||
}
|
||||
|
||||
// Here we must interpret each typed character individually, that's why we create a new context
|
||||
// Here we must interpret each typed character individually
|
||||
const autoClosedCharacters = AutoClosedAction.getAllAutoClosedCharacters(this._autoClosedActions);
|
||||
this._executeEditOperation(TypeOperations.typeWithInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), autoClosedCharacters, chr));
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ICommand, IConfiguration, ScrollType } from 'vs/editor/common/editorCom
|
||||
import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { LanguageIdentifier } from 'vs/editor/common/modes';
|
||||
import { IAutoClosingPair } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { IAutoClosingPair, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { VerticalRevealType } from 'vs/editor/common/view/viewEvents';
|
||||
import { IViewModel } from 'vs/editor/common/viewModel/viewModel';
|
||||
@@ -67,11 +67,22 @@ export interface ICursors {
|
||||
export interface CharacterMap {
|
||||
[char: string]: string;
|
||||
}
|
||||
export interface MultipleCharacterMap {
|
||||
[char: string]: string[];
|
||||
}
|
||||
|
||||
const autoCloseAlways = () => true;
|
||||
const autoCloseNever = () => false;
|
||||
const autoCloseBeforeWhitespace = (chr: string) => (chr === ' ' || chr === '\t');
|
||||
|
||||
function appendEntry<K, V>(target: Map<K, V[]>, key: K, value: V): void {
|
||||
if (target.has(key)) {
|
||||
target.get(key)!.push(value);
|
||||
} else {
|
||||
target.set(key, [value]);
|
||||
}
|
||||
}
|
||||
|
||||
export class CursorConfiguration {
|
||||
_cursorMoveConfigurationBrand: void;
|
||||
|
||||
@@ -90,8 +101,8 @@ export class CursorConfiguration {
|
||||
public readonly autoClosingQuotes: EditorAutoClosingStrategy;
|
||||
public readonly autoSurround: EditorAutoSurroundStrategy;
|
||||
public readonly autoIndent: boolean;
|
||||
public readonly autoClosingPairsOpen: CharacterMap;
|
||||
public readonly autoClosingPairsClose: CharacterMap;
|
||||
public readonly autoClosingPairsOpen2: Map<string, StandardAutoClosingPairConditional[]>;
|
||||
public readonly autoClosingPairsClose2: Map<string, StandardAutoClosingPairConditional[]>;
|
||||
public readonly surroundingPairs: CharacterMap;
|
||||
public readonly shouldAutoCloseBefore: { quote: (ch: string) => boolean, bracket: (ch: string) => boolean };
|
||||
|
||||
@@ -138,8 +149,8 @@ export class CursorConfiguration {
|
||||
this.autoSurround = c.autoSurround;
|
||||
this.autoIndent = c.autoIndent;
|
||||
|
||||
this.autoClosingPairsOpen = {};
|
||||
this.autoClosingPairsClose = {};
|
||||
this.autoClosingPairsOpen2 = new Map<string, StandardAutoClosingPairConditional[]>();
|
||||
this.autoClosingPairsClose2 = new Map<string, StandardAutoClosingPairConditional[]>();
|
||||
this.surroundingPairs = {};
|
||||
this._electricChars = null;
|
||||
|
||||
@@ -151,8 +162,10 @@ export class CursorConfiguration {
|
||||
let autoClosingPairs = CursorConfiguration._getAutoClosingPairs(languageIdentifier);
|
||||
if (autoClosingPairs) {
|
||||
for (const pair of autoClosingPairs) {
|
||||
this.autoClosingPairsOpen[pair.open] = pair.close;
|
||||
this.autoClosingPairsClose[pair.close] = pair.open;
|
||||
appendEntry(this.autoClosingPairsOpen2, pair.open.charAt(pair.open.length - 1), pair);
|
||||
if (pair.close.length === 1) {
|
||||
appendEntry(this.autoClosingPairsClose2, pair.close, pair);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +203,7 @@ export class CursorConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
private static _getAutoClosingPairs(languageIdentifier: LanguageIdentifier): IAutoClosingPair[] | null {
|
||||
private static _getAutoClosingPairs(languageIdentifier: LanguageIdentifier): StandardAutoClosingPairConditional[] | null {
|
||||
try {
|
||||
return LanguageConfigurationRegistry.getAutoClosingPairs(languageIdentifier.id);
|
||||
} catch (e) {
|
||||
|
||||
@@ -63,7 +63,8 @@ export class DeleteOperations {
|
||||
const lineText = model.getLineContent(position.lineNumber);
|
||||
const character = lineText[position.column - 2];
|
||||
|
||||
if (!config.autoClosingPairsOpen.hasOwnProperty(character)) {
|
||||
const autoClosingPairCandidates = config.autoClosingPairsOpen2.get(character);
|
||||
if (!autoClosingPairCandidates) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -78,9 +79,14 @@ export class DeleteOperations {
|
||||
}
|
||||
|
||||
const afterCharacter = lineText[position.column - 1];
|
||||
const closeCharacter = config.autoClosingPairsOpen[character];
|
||||
|
||||
if (afterCharacter !== closeCharacter) {
|
||||
let foundAutoClosingPair = false;
|
||||
for (const autoClosingPairCandidate of autoClosingPairCandidates) {
|
||||
if (autoClosingPairCandidate.open === character && autoClosingPairCandidate.close === afterCharacter) {
|
||||
foundAutoClosingPair = true;
|
||||
}
|
||||
}
|
||||
if (!foundAutoClosingPair) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,10 @@ import { CursorColumns, CursorConfiguration, EditOperationResult, EditOperationT
|
||||
import { WordCharacterClass, getMapForWordSeparators } from 'vs/editor/common/controller/wordCharacterClassifier';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { ICommand, ICursorStateComputerData } from 'vs/editor/common/editorCommon';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { EnterAction, IndentAction } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { EnterAction, IndentAction, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { IElectricAction } from 'vs/editor/common/modes/supports/electricCharacter';
|
||||
|
||||
@@ -433,7 +434,11 @@ export class TypeOperations {
|
||||
private static _isAutoClosingCloseCharType(config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): boolean {
|
||||
const autoCloseConfig = isQuote(ch) ? config.autoClosingQuotes : config.autoClosingBrackets;
|
||||
|
||||
if (autoCloseConfig === 'never' || !config.autoClosingPairsClose.hasOwnProperty(ch)) {
|
||||
if (autoCloseConfig === 'never') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.autoClosingPairsClose2.has(ch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -469,15 +474,6 @@ export class TypeOperations {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static _countNeedlesInHaystack(haystack: string, needle: string): number {
|
||||
let cnt = 0;
|
||||
let lastIndex = -1;
|
||||
while ((lastIndex = haystack.indexOf(needle, lastIndex + 1)) !== -1) {
|
||||
cnt++;
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
|
||||
private static _runAutoClosingCloseCharType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): EditOperationResult {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
@@ -492,65 +488,98 @@ export class TypeOperations {
|
||||
});
|
||||
}
|
||||
|
||||
private static _isBeforeClosingBrace(config: CursorConfiguration, ch: string, characterAfter: string) {
|
||||
const thisBraceIsSymmetric = (config.autoClosingPairsOpen[ch] === ch);
|
||||
let isBeforeCloseBrace = false;
|
||||
for (let otherCloseBrace in config.autoClosingPairsClose) {
|
||||
const otherBraceIsSymmetric = (config.autoClosingPairsOpen[otherCloseBrace] === otherCloseBrace);
|
||||
if (!thisBraceIsSymmetric && otherBraceIsSymmetric) {
|
||||
continue;
|
||||
}
|
||||
if (characterAfter === otherCloseBrace) {
|
||||
isBeforeCloseBrace = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return isBeforeCloseBrace;
|
||||
}
|
||||
|
||||
private static _isAutoClosingOpenCharType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): boolean {
|
||||
const chIsQuote = isQuote(ch);
|
||||
const autoCloseConfig = chIsQuote ? config.autoClosingQuotes : config.autoClosingBrackets;
|
||||
|
||||
if (autoCloseConfig === 'never' || !config.autoClosingPairsOpen.hasOwnProperty(ch)) {
|
||||
private static _isBeforeClosingBrace(config: CursorConfiguration, autoClosingPair: StandardAutoClosingPairConditional, characterAfter: string) {
|
||||
const otherAutoClosingPairs = config.autoClosingPairsClose2.get(characterAfter);
|
||||
if (!otherAutoClosingPairs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let shouldAutoCloseBefore = chIsQuote ? config.shouldAutoCloseBefore.quote : config.shouldAutoCloseBefore.bracket;
|
||||
const thisBraceIsSymmetric = (autoClosingPair.open === autoClosingPair.close);
|
||||
for (const otherAutoClosingPair of otherAutoClosingPairs) {
|
||||
const otherBraceIsSymmetric = (otherAutoClosingPair.open === otherAutoClosingPair.close);
|
||||
if (!thisBraceIsSymmetric && otherBraceIsSymmetric) {
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static _findAutoClosingPairOpen(config: CursorConfiguration, model: ITextModel, positions: Position[], ch: string): StandardAutoClosingPairConditional | null {
|
||||
const autoClosingPairCandidates = config.autoClosingPairsOpen2.get(ch);
|
||||
if (!autoClosingPairCandidates) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine which auto-closing pair it is
|
||||
let autoClosingPair: StandardAutoClosingPairConditional | null = null;
|
||||
for (const autoClosingPairCandidate of autoClosingPairCandidates) {
|
||||
if (autoClosingPair === null || autoClosingPairCandidate.open.length > autoClosingPair.open.length) {
|
||||
let candidateIsMatch = true;
|
||||
for (const position of positions) {
|
||||
const relevantText = model.getValueInRange(new Range(position.lineNumber, position.column - autoClosingPairCandidate.open.length + 1, position.lineNumber, position.column));
|
||||
if (relevantText + ch !== autoClosingPairCandidate.open) {
|
||||
candidateIsMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidateIsMatch) {
|
||||
autoClosingPair = autoClosingPairCandidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return autoClosingPair;
|
||||
}
|
||||
|
||||
private static _isAutoClosingOpenCharType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, insertOpenCharacter: boolean): StandardAutoClosingPairConditional | null {
|
||||
const chIsQuote = isQuote(ch);
|
||||
const autoCloseConfig = chIsQuote ? config.autoClosingQuotes : config.autoClosingBrackets;
|
||||
if (autoCloseConfig === 'never') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const autoClosingPair = this._findAutoClosingPairOpen(config, model, selections.map(s => s.getPosition()), ch);
|
||||
if (!autoClosingPair) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const shouldAutoCloseBefore = chIsQuote ? config.shouldAutoCloseBefore.quote : config.shouldAutoCloseBefore.bracket;
|
||||
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
if (!selection.isEmpty()) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const position = selection.getPosition();
|
||||
const lineText = model.getLineContent(position.lineNumber);
|
||||
|
||||
// Do not auto-close ' or " after a word character
|
||||
if ((chIsQuote && position.column > 1) && autoCloseConfig !== 'always') {
|
||||
const wordSeparators = getMapForWordSeparators(config.wordSeparators);
|
||||
const characterBeforeCode = lineText.charCodeAt(position.column - 2);
|
||||
const characterBeforeType = wordSeparators.get(characterBeforeCode);
|
||||
if (characterBeforeType === WordCharacterClass.Regular) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Only consider auto closing the pair if a space follows or if another autoclosed pair follows
|
||||
const characterAfter = lineText.charAt(position.column - 1);
|
||||
if (characterAfter) {
|
||||
let isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, ch, characterAfter);
|
||||
if (lineText.length > position.column - 1) {
|
||||
const characterAfter = lineText.charAt(position.column - 1);
|
||||
const isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, autoClosingPair, characterAfter);
|
||||
|
||||
if (!isBeforeCloseBrace && !shouldAutoCloseBefore(characterAfter)) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!model.isCheapToTokenize(position.lineNumber)) {
|
||||
// Do not force tokenization
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Do not auto-close ' or " after a word character
|
||||
if (autoClosingPair.open.length === 1 && chIsQuote && autoCloseConfig !== 'always') {
|
||||
const wordSeparators = getMapForWordSeparators(config.wordSeparators);
|
||||
if (insertOpenCharacter && position.column > 1 && wordSeparators.get(lineText.charCodeAt(position.column - 2)) === WordCharacterClass.Regular) {
|
||||
return null;
|
||||
}
|
||||
if (!insertOpenCharacter && position.column > 2 && wordSeparators.get(lineText.charCodeAt(position.column - 3)) === WordCharacterClass.Regular) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
model.forceTokenization(position.lineNumber);
|
||||
@@ -558,25 +587,24 @@ export class TypeOperations {
|
||||
|
||||
let shouldAutoClosePair = false;
|
||||
try {
|
||||
shouldAutoClosePair = LanguageConfigurationRegistry.shouldAutoClosePair(ch, lineTokens, position.column);
|
||||
shouldAutoClosePair = LanguageConfigurationRegistry.shouldAutoClosePair(autoClosingPair, lineTokens, insertOpenCharacter ? position.column : position.column - 1);
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
}
|
||||
|
||||
if (!shouldAutoClosePair) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return autoClosingPair;
|
||||
}
|
||||
|
||||
private static _runAutoClosingOpenCharType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): EditOperationResult {
|
||||
private static _runAutoClosingOpenCharType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, insertOpenCharacter: boolean, autoClosingPair: StandardAutoClosingPairConditional): EditOperationResult {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
const closeCharacter = config.autoClosingPairsOpen[ch];
|
||||
commands[i] = new TypeWithAutoClosingCommand(selection, ch, closeCharacter);
|
||||
commands[i] = new TypeWithAutoClosingCommand(selection, ch, insertOpenCharacter, autoClosingPair.close);
|
||||
}
|
||||
return new EditOperationResult(EditOperationType.Typing, commands, {
|
||||
shouldPushStackElementBefore: true,
|
||||
@@ -679,14 +707,6 @@ export class TypeOperations {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (electricAction.appendText) {
|
||||
const command = new ReplaceCommandWithOffsetCursorState(selection, ch + electricAction.appendText, 0, -electricAction.appendText.length);
|
||||
return new EditOperationResult(EditOperationType.Typing, [command], {
|
||||
shouldPushStackElementBefore: false,
|
||||
shouldPushStackElementAfter: true
|
||||
});
|
||||
}
|
||||
|
||||
if (electricAction.matchOpenBracket) {
|
||||
let endColumn = (lineTokens.getLineContent() + ch).lastIndexOf(electricAction.matchOpenBracket) + 1;
|
||||
let match = model.findMatchingBracketUp(electricAction.matchOpenBracket, {
|
||||
@@ -722,87 +742,44 @@ export class TypeOperations {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static compositionEndWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[]): EditOperationResult | null {
|
||||
if (config.autoClosingQuotes === 'never') {
|
||||
/**
|
||||
* This is very similar with typing, but the character is already in the text buffer!
|
||||
*/
|
||||
public static compositionEndWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[]): EditOperationResult | null {
|
||||
let ch: string | null = null;
|
||||
// extract last typed character
|
||||
for (const selection of selections) {
|
||||
if (!selection.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
const position = selection.getPosition();
|
||||
const currentChar = model.getValueInRange(new Range(position.lineNumber, position.column - 1, position.lineNumber, position.column));
|
||||
if (ch === null) {
|
||||
ch = currentChar;
|
||||
} else if (ch !== currentChar) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let commands: ICommand[] = [];
|
||||
|
||||
for (let i = 0; i < selections.length; i++) {
|
||||
if (!selections[i].isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const position = selections[i].getPosition();
|
||||
const lineText = model.getLineContent(position.lineNumber);
|
||||
const ch = lineText.charAt(position.column - 2);
|
||||
|
||||
if (config.autoClosingPairsClose.hasOwnProperty(ch)) { // first of all, it's a closing tag
|
||||
if (ch === config.autoClosingPairsClose[ch] /** isEqualPair */) {
|
||||
const lineTextBeforeCursor = lineText.substr(0, position.column - 2);
|
||||
const chCntBefore = this._countNeedlesInHaystack(lineTextBeforeCursor, ch);
|
||||
|
||||
if (chCntBefore % 2 === 1) {
|
||||
continue; // it pairs with the opening tag.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// As we are not typing in a new character, so we don't need to run `_runAutoClosingCloseCharType`
|
||||
// Next step, let's try to check if it's an open char.
|
||||
if (config.autoClosingPairsOpen.hasOwnProperty(ch)) {
|
||||
if (isQuote(ch) && position.column > 2) {
|
||||
const wordSeparators = getMapForWordSeparators(config.wordSeparators);
|
||||
const characterBeforeCode = lineText.charCodeAt(position.column - 3);
|
||||
const characterBeforeType = wordSeparators.get(characterBeforeCode);
|
||||
if (characterBeforeType === WordCharacterClass.Regular) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const characterAfter = lineText.charAt(position.column - 1);
|
||||
|
||||
if (characterAfter) {
|
||||
let isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, ch, characterAfter);
|
||||
let shouldAutoCloseBefore = isQuote(ch) ? config.shouldAutoCloseBefore.quote : config.shouldAutoCloseBefore.bracket;
|
||||
if (isBeforeCloseBrace) {
|
||||
// In normal auto closing logic, we will auto close if the cursor is even before a closing brace intentionally.
|
||||
// However for composition mode, we do nothing here as users might clear all the characters for composition and we don't want to do a unnecessary auto close.
|
||||
// Related: microsoft/vscode#57250.
|
||||
continue;
|
||||
}
|
||||
if (!shouldAutoCloseBefore(characterAfter)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!model.isCheapToTokenize(position.lineNumber)) {
|
||||
// Do not force tokenization
|
||||
continue;
|
||||
}
|
||||
|
||||
model.forceTokenization(position.lineNumber);
|
||||
const lineTokens = model.getLineTokens(position.lineNumber);
|
||||
|
||||
let shouldAutoClosePair = false;
|
||||
|
||||
try {
|
||||
shouldAutoClosePair = LanguageConfigurationRegistry.shouldAutoClosePair(ch, lineTokens, position.column - 1);
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
}
|
||||
|
||||
if (shouldAutoClosePair) {
|
||||
const closeCharacter = config.autoClosingPairsOpen[ch];
|
||||
commands[i] = new ReplaceCommandWithOffsetCursorState(selections[i], closeCharacter, 0, -closeCharacter.length);
|
||||
}
|
||||
}
|
||||
if (this._isAutoClosingCloseCharType(config, model, selections, autoClosedCharacters, ch)) {
|
||||
// Unfortunately, the close character is at this point "doubled", so we need to delete it...
|
||||
const commands = selections.map(s => new ReplaceCommand(new Range(s.positionLineNumber, s.positionColumn, s.positionLineNumber, s.positionColumn + 1), '', false));
|
||||
return new EditOperationResult(EditOperationType.Typing, commands, {
|
||||
shouldPushStackElementBefore: true,
|
||||
shouldPushStackElementAfter: false
|
||||
});
|
||||
}
|
||||
|
||||
return new EditOperationResult(EditOperationType.Typing, commands, {
|
||||
shouldPushStackElementBefore: true,
|
||||
shouldPushStackElementAfter: false
|
||||
});
|
||||
const autoClosingPairOpenCharType = this._isAutoClosingOpenCharType(config, model, selections, ch, false);
|
||||
if (autoClosingPairOpenCharType) {
|
||||
return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, false, autoClosingPairOpenCharType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static typeWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult {
|
||||
@@ -840,8 +817,9 @@ export class TypeOperations {
|
||||
return this._runAutoClosingCloseCharType(prevEditOperationType, config, model, selections, ch);
|
||||
}
|
||||
|
||||
if (this._isAutoClosingOpenCharType(config, model, selections, ch)) {
|
||||
return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch);
|
||||
const autoClosingPairOpenCharType = this._isAutoClosingOpenCharType(config, model, selections, ch, true);
|
||||
if (autoClosingPairOpenCharType) {
|
||||
return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, true, autoClosingPairOpenCharType);
|
||||
}
|
||||
|
||||
if (this._isSurroundSelectionType(config, model, selections, ch)) {
|
||||
@@ -929,12 +907,14 @@ export class TypeOperations {
|
||||
|
||||
export class TypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorState {
|
||||
|
||||
private _closeCharacter: string;
|
||||
private readonly _openCharacter: string;
|
||||
private readonly _closeCharacter: string;
|
||||
public closeCharacterRange: Range | null;
|
||||
public enclosingRange: Range | null;
|
||||
|
||||
constructor(selection: Selection, openCharacter: string, closeCharacter: string) {
|
||||
super(selection, openCharacter + closeCharacter, 0, -closeCharacter.length);
|
||||
constructor(selection: Selection, openCharacter: string, insertOpenCharacter: boolean, closeCharacter: string) {
|
||||
super(selection, (insertOpenCharacter ? openCharacter : '') + closeCharacter, 0, -closeCharacter.length);
|
||||
this._openCharacter = openCharacter;
|
||||
this._closeCharacter = closeCharacter;
|
||||
this.closeCharacterRange = null;
|
||||
this.enclosingRange = null;
|
||||
@@ -944,7 +924,7 @@ export class TypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorSt
|
||||
let inverseEditOperations = helper.getInverseEditOperations();
|
||||
let range = inverseEditOperations[0].range;
|
||||
this.closeCharacterRange = new Range(range.startLineNumber, range.endColumn - this._closeCharacter.length, range.endLineNumber, range.endColumn);
|
||||
this.enclosingRange = range;
|
||||
this.enclosingRange = new Range(range.startLineNumber, range.endColumn - this._openCharacter.length - this._closeCharacter.length, range.endLineNumber, range.endColumn);
|
||||
return super.computeCursorState(model, helper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,9 @@ export interface LanguageConfiguration {
|
||||
*
|
||||
* @deprecated Will be replaced by a better API soon.
|
||||
*/
|
||||
__electricCharacterSupport?: IBracketElectricCharacterContribution;
|
||||
__electricCharacterSupport?: {
|
||||
docComment?: IDocComment;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,10 +157,6 @@ export interface OnEnterRule {
|
||||
action: EnterAction;
|
||||
}
|
||||
|
||||
export interface IBracketElectricCharacterContribution {
|
||||
docComment?: IDocComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Definition of documentation comments (e.g. Javadoc/JSdoc)
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Range } from 'vs/editor/common/core/range';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { DEFAULT_WORD_REGEXP, ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper';
|
||||
import { LanguageId, LanguageIdentifier } from 'vs/editor/common/modes';
|
||||
import { EnterAction, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentAction, IndentationRule, LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { EnterAction, FoldingRules, IAutoClosingPair, IndentAction, IndentationRule, LanguageConfiguration, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { createScopedLineTokens } from 'vs/editor/common/modes/supports';
|
||||
import { CharacterPairSupport } from 'vs/editor/common/modes/supports/characterPair';
|
||||
import { BracketElectricCharacterSupport, IElectricAction } from 'vs/editor/common/modes/supports/electricCharacter';
|
||||
@@ -97,16 +97,7 @@ export class RichEditSupport {
|
||||
|
||||
public get electricCharacter(): BracketElectricCharacterSupport | null {
|
||||
if (!this._electricCharacter) {
|
||||
let autoClosingPairs: IAutoClosingPairConditional[] = [];
|
||||
if (this._conf.autoClosingPairs) {
|
||||
autoClosingPairs = this._conf.autoClosingPairs;
|
||||
} else if (this._conf.brackets) {
|
||||
autoClosingPairs = this._conf.brackets.map(b => {
|
||||
return { open: b[0], close: b[1] };
|
||||
});
|
||||
}
|
||||
|
||||
this._electricCharacter = new BracketElectricCharacterSupport(this.brackets, autoClosingPairs, this._conf.__electricCharacterSupport);
|
||||
this._electricCharacter = new BracketElectricCharacterSupport(this.brackets);
|
||||
}
|
||||
return this._electricCharacter;
|
||||
}
|
||||
@@ -261,7 +252,7 @@ export class LanguageConfigurationRegistryImpl {
|
||||
return value.characterPair || null;
|
||||
}
|
||||
|
||||
public getAutoClosingPairs(languageId: LanguageId): IAutoClosingPair[] {
|
||||
public getAutoClosingPairs(languageId: LanguageId): StandardAutoClosingPairConditional[] {
|
||||
let characterPairSupport = this._getCharacterPairSupport(languageId);
|
||||
if (!characterPairSupport) {
|
||||
return [];
|
||||
@@ -285,13 +276,9 @@ export class LanguageConfigurationRegistryImpl {
|
||||
return characterPairSupport.getSurroundingPairs();
|
||||
}
|
||||
|
||||
public shouldAutoClosePair(character: string, context: LineTokens, column: number): boolean {
|
||||
let scopedLineTokens = createScopedLineTokens(context, column - 1);
|
||||
let characterPairSupport = this._getCharacterPairSupport(scopedLineTokens.languageId);
|
||||
if (!characterPairSupport) {
|
||||
return false;
|
||||
}
|
||||
return characterPairSupport.shouldAutoClosePair(character, scopedLineTokens, column - scopedLineTokens.firstCharOffset);
|
||||
public shouldAutoClosePair(autoClosingPair: StandardAutoClosingPairConditional, context: LineTokens, column: number): boolean {
|
||||
const scopedLineTokens = createScopedLineTokens(context, column - 1);
|
||||
return CharacterPairSupport.shouldAutoClosePair(autoClosingPair, scopedLineTokens, column - scopedLineTokens.firstCharOffset);
|
||||
}
|
||||
|
||||
// end characterPair
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CharacterPair, IAutoClosingPair, IAutoClosingPairConditional, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { IAutoClosingPair, StandardAutoClosingPairConditional, LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { ScopedLineTokens } from 'vs/editor/common/modes/supports';
|
||||
|
||||
export class CharacterPairSupport {
|
||||
@@ -15,7 +15,7 @@ export class CharacterPairSupport {
|
||||
private readonly _surroundingPairs: IAutoClosingPair[];
|
||||
private readonly _autoCloseBefore: string;
|
||||
|
||||
constructor(config: { brackets?: CharacterPair[]; autoClosingPairs?: IAutoClosingPairConditional[], surroundingPairs?: IAutoClosingPair[], autoCloseBefore?: string }) {
|
||||
constructor(config: LanguageConfiguration) {
|
||||
if (config.autoClosingPairs) {
|
||||
this._autoClosingPairs = config.autoClosingPairs.map(el => new StandardAutoClosingPairConditional(el));
|
||||
} else if (config.brackets) {
|
||||
@@ -24,12 +24,18 @@ export class CharacterPairSupport {
|
||||
this._autoClosingPairs = [];
|
||||
}
|
||||
|
||||
if (config.__electricCharacterSupport && config.__electricCharacterSupport.docComment) {
|
||||
const docComment = config.__electricCharacterSupport.docComment;
|
||||
// IDocComment is legacy, only partially supported
|
||||
this._autoClosingPairs.push(new StandardAutoClosingPairConditional({ open: docComment.open, close: docComment.close || '' }));
|
||||
}
|
||||
|
||||
this._autoCloseBefore = typeof config.autoCloseBefore === 'string' ? config.autoCloseBefore : CharacterPairSupport.DEFAULT_AUTOCLOSE_BEFORE_LANGUAGE_DEFINED;
|
||||
|
||||
this._surroundingPairs = config.surroundingPairs || this._autoClosingPairs;
|
||||
}
|
||||
|
||||
public getAutoClosingPairs(): IAutoClosingPair[] {
|
||||
public getAutoClosingPairs(): StandardAutoClosingPairConditional[] {
|
||||
return this._autoClosingPairs;
|
||||
}
|
||||
|
||||
@@ -37,22 +43,15 @@ export class CharacterPairSupport {
|
||||
return this._autoCloseBefore;
|
||||
}
|
||||
|
||||
public shouldAutoClosePair(character: string, context: ScopedLineTokens, column: number): boolean {
|
||||
public static shouldAutoClosePair(autoClosingPair: StandardAutoClosingPairConditional, context: ScopedLineTokens, column: number): boolean {
|
||||
// Always complete on empty line
|
||||
if (context.getTokenCount() === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let tokenIndex = context.findTokenIndexAtOffset(column - 2);
|
||||
let standardTokenType = context.getStandardTokenType(tokenIndex);
|
||||
|
||||
for (const autoClosingPair of this._autoClosingPairs) {
|
||||
if (autoClosingPair.open === character) {
|
||||
return autoClosingPair.isOK(standardTokenType);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
const tokenIndex = context.findTokenIndexAtOffset(column - 2);
|
||||
const standardTokenType = context.getStandardTokenType(tokenIndex);
|
||||
return autoClosingPair.isOK(standardTokenType);
|
||||
}
|
||||
|
||||
public getSurroundingPairs(): IAutoClosingPair[] {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IAutoClosingPairConditional, IBracketElectricCharacterContribution, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { ScopedLineTokens, ignoreBracketsInToken } from 'vs/editor/common/modes/supports';
|
||||
import { BracketsUtils, RichEditBrackets } from 'vs/editor/common/modes/supports/richEditBrackets';
|
||||
|
||||
@@ -12,29 +11,17 @@ import { BracketsUtils, RichEditBrackets } from 'vs/editor/common/modes/supports
|
||||
* @internal
|
||||
*/
|
||||
export interface IElectricAction {
|
||||
// Only one of the following properties should be defined:
|
||||
|
||||
// The line will be indented at the same level of the line
|
||||
// which contains the matching given bracket type.
|
||||
matchOpenBracket?: string;
|
||||
|
||||
// The text will be appended after the electric character.
|
||||
appendText?: string;
|
||||
matchOpenBracket: string;
|
||||
}
|
||||
|
||||
export class BracketElectricCharacterSupport {
|
||||
|
||||
private readonly _richEditBrackets: RichEditBrackets | null;
|
||||
private readonly _complexAutoClosePairs: StandardAutoClosingPairConditional[];
|
||||
|
||||
constructor(richEditBrackets: RichEditBrackets | null, autoClosePairs: IAutoClosingPairConditional[], contribution: IBracketElectricCharacterContribution | null | undefined) {
|
||||
contribution = contribution || {};
|
||||
constructor(richEditBrackets: RichEditBrackets | null) {
|
||||
this._richEditBrackets = richEditBrackets;
|
||||
this._complexAutoClosePairs = autoClosePairs.filter(pair => pair.open.length > 1 && !!pair.close).map(el => new StandardAutoClosingPairConditional(el));
|
||||
if (contribution.docComment) {
|
||||
// IDocComment is legacy, only partially supported
|
||||
this._complexAutoClosePairs.push(new StandardAutoClosingPairConditional({ open: contribution.docComment.open, close: contribution.docComment.close || '' }));
|
||||
}
|
||||
}
|
||||
|
||||
public getElectricCharacters(): string[] {
|
||||
@@ -48,11 +35,6 @@ export class BracketElectricCharacterSupport {
|
||||
}
|
||||
}
|
||||
|
||||
// auto close
|
||||
for (let pair of this._complexAutoClosePairs) {
|
||||
result.push(pair.open.charAt(pair.open.length - 1));
|
||||
}
|
||||
|
||||
// Filter duplicate entries
|
||||
result = result.filter((item, pos, array) => {
|
||||
return array.indexOf(item) === pos;
|
||||
@@ -62,12 +44,6 @@ export class BracketElectricCharacterSupport {
|
||||
}
|
||||
|
||||
public onElectricCharacter(character: string, context: ScopedLineTokens, column: number): IElectricAction | null {
|
||||
return (this._onElectricAutoClose(character, context, column) ||
|
||||
this._onElectricAutoIndent(character, context, column));
|
||||
}
|
||||
|
||||
private _onElectricAutoIndent(character: string, context: ScopedLineTokens, column: number): IElectricAction | null {
|
||||
|
||||
if (!this._richEditBrackets || this._richEditBrackets.brackets.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -103,44 +79,4 @@ export class BracketElectricCharacterSupport {
|
||||
matchOpenBracket: bracketText
|
||||
};
|
||||
}
|
||||
|
||||
private _onElectricAutoClose(character: string, context: ScopedLineTokens, column: number): IElectricAction | null {
|
||||
if (!this._complexAutoClosePairs.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let line = context.getLineContent();
|
||||
|
||||
for (let i = 0, len = this._complexAutoClosePairs.length; i < len; i++) {
|
||||
let pair = this._complexAutoClosePairs[i];
|
||||
|
||||
// See if the right electric character was pressed
|
||||
if (character !== pair.open.charAt(pair.open.length - 1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if the full open bracket matches
|
||||
let start = column - pair.open.length + 1;
|
||||
let actual = line.substring(start - 1, column - 1) + character;
|
||||
if (actual !== pair.open) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lastTokenIndex = context.findTokenIndexAtOffset(column - 1);
|
||||
let lastTokenStandardType = context.getStandardTokenType(lastTokenIndex);
|
||||
// If we're in a scope listed in 'notIn', do nothing
|
||||
if (!pair.isOK(lastTokenStandardType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this line already contains the closing tag, do nothing.
|
||||
if (line.indexOf(pair.close, column - 1) >= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return { appendText: pair.close };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,16 +202,33 @@ export class OutlineGroup extends TreeElement {
|
||||
}
|
||||
}
|
||||
|
||||
class MovingAverage {
|
||||
|
||||
private _n = 1;
|
||||
private _val = 0;
|
||||
|
||||
update(value: number): this {
|
||||
this._val = this._val + (value - this._val) / this._n;
|
||||
this._n += 1;
|
||||
return this;
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._val;
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlineModel extends TreeElement {
|
||||
|
||||
private static readonly _requestDurations = new LRUCache<string, MovingAverage>(50, 0.7);
|
||||
private static readonly _requests = new LRUCache<string, { promiseCnt: number, source: CancellationTokenSource, promise: Promise<any>, model: OutlineModel | undefined }>(9, 0.75);
|
||||
private static readonly _keys = new class {
|
||||
|
||||
private _counter = 1;
|
||||
private _data = new WeakMap<DocumentSymbolProvider, number>();
|
||||
|
||||
for(textModel: ITextModel): string {
|
||||
return `${textModel.id}/${textModel.getVersionId()}/${this._hash(DocumentSymbolProviderRegistry.all(textModel))}`;
|
||||
for(textModel: ITextModel, version: boolean): string {
|
||||
return `${textModel.id}/${version ? textModel.getVersionId() : ''}/${this._hash(DocumentSymbolProviderRegistry.all(textModel))}`;
|
||||
}
|
||||
|
||||
private _hash(providers: DocumentSymbolProvider[]): string {
|
||||
@@ -231,7 +248,7 @@ export class OutlineModel extends TreeElement {
|
||||
|
||||
static create(textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {
|
||||
|
||||
let key = this._keys.for(textModel);
|
||||
let key = this._keys.for(textModel, true);
|
||||
let data = OutlineModel._requests.get(key);
|
||||
|
||||
if (!data) {
|
||||
@@ -243,6 +260,18 @@ export class OutlineModel extends TreeElement {
|
||||
model: undefined,
|
||||
};
|
||||
OutlineModel._requests.set(key, data);
|
||||
|
||||
// keep moving average of request durations
|
||||
const now = Date.now();
|
||||
data.promise.then(() => {
|
||||
let key = this._keys.for(textModel, false);
|
||||
let avg = this._requestDurations.get(key);
|
||||
if (!avg) {
|
||||
avg = new MovingAverage();
|
||||
this._requestDurations.set(key, avg);
|
||||
}
|
||||
avg.update(Date.now() - now);
|
||||
});
|
||||
}
|
||||
|
||||
if (data!.model) {
|
||||
@@ -272,7 +301,18 @@ export class OutlineModel extends TreeElement {
|
||||
});
|
||||
}
|
||||
|
||||
static _create(textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {
|
||||
static getRequestDelay(textModel: ITextModel | null): number {
|
||||
if (!textModel) {
|
||||
return 350;
|
||||
}
|
||||
const avg = this._requestDurations.get(this._keys.for(textModel, false));
|
||||
if (!avg) {
|
||||
return 350;
|
||||
}
|
||||
return Math.max(350, Math.floor(1.3 * avg.value));
|
||||
}
|
||||
|
||||
private static _create(textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {
|
||||
|
||||
const cts = new CancellationTokenSource(token);
|
||||
const result = new OutlineModel(textModel);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import { containsUppercaseCharacter } from 'vs/base/common/strings';
|
||||
import { buildReplaceStringWithCasePreserved } from 'vs/base/common/search';
|
||||
|
||||
const enum ReplacePatternKind {
|
||||
StaticValue = 0,
|
||||
@@ -51,17 +51,8 @@ export class ReplacePattern {
|
||||
|
||||
public buildReplaceString(matches: string[] | null, preserveCase?: boolean): string {
|
||||
if (this._state.kind === ReplacePatternKind.StaticValue) {
|
||||
if (preserveCase && matches && (matches[0] !== '')) {
|
||||
if (matches[0].toUpperCase() === matches[0]) {
|
||||
return this._state.staticValue.toUpperCase();
|
||||
} else if (matches[0].toLowerCase() === matches[0]) {
|
||||
return this._state.staticValue.toLowerCase();
|
||||
} else if (containsUppercaseCharacter(matches[0][0])) {
|
||||
return this._state.staticValue[0].toUpperCase() + this._state.staticValue.substr(1);
|
||||
} else {
|
||||
// we don't understand its pattern yet.
|
||||
return this._state.staticValue;
|
||||
}
|
||||
if (preserveCase) {
|
||||
return buildReplaceStringWithCasePreserved(matches, this._state.staticValue);
|
||||
} else {
|
||||
return this._state.staticValue;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { ReplacePattern, ReplacePiece, parseReplaceString } from 'vs/editor/contrib/find/replacePattern';
|
||||
import { buildReplaceStringWithCasePreserved } from 'vs/base/common/search';
|
||||
|
||||
suite('Replace Pattern test', () => {
|
||||
|
||||
@@ -154,6 +155,29 @@ suite('Replace Pattern test', () => {
|
||||
assert.equal(actual, 'a{}');
|
||||
});
|
||||
|
||||
test('buildReplaceStringWithCasePreserved test', () => {
|
||||
let replacePattern = 'Def';
|
||||
let actual: string | string[] = 'abc';
|
||||
|
||||
assert.equal(buildReplaceStringWithCasePreserved([actual], replacePattern), 'def');
|
||||
actual = 'Abc';
|
||||
assert.equal(buildReplaceStringWithCasePreserved([actual], replacePattern), 'Def');
|
||||
actual = 'ABC';
|
||||
assert.equal(buildReplaceStringWithCasePreserved([actual], replacePattern), 'DEF');
|
||||
|
||||
actual = ['abc', 'Abc'];
|
||||
assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'def');
|
||||
actual = ['Abc', 'abc'];
|
||||
assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'Def');
|
||||
actual = ['ABC', 'abc'];
|
||||
assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'DEF');
|
||||
|
||||
actual = ['AbC'];
|
||||
assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'Def');
|
||||
actual = ['aBC'];
|
||||
assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'Def');
|
||||
});
|
||||
|
||||
test('preserve case', () => {
|
||||
let replacePattern = parseReplaceString('Def');
|
||||
let actual = replacePattern.buildReplaceString(['abc'], true);
|
||||
|
||||
@@ -105,7 +105,11 @@ export class RangesCollector {
|
||||
}
|
||||
|
||||
|
||||
interface PreviousRegion { indent: number; line: number; marker: boolean; }
|
||||
interface PreviousRegion {
|
||||
indent: number; // indent or -2 if a marker
|
||||
endAbove: number; // end line number for the region above
|
||||
line: number; // start line of the region. Only used for marker regions.
|
||||
}
|
||||
|
||||
export function computeRanges(model: ITextModel, offSide: boolean, markers?: FoldingMarkers, foldingRangesLimit = MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT): FoldingRegions {
|
||||
const tabSize = model.getOptions().tabSize;
|
||||
@@ -117,16 +121,19 @@ export function computeRanges(model: ITextModel, offSide: boolean, markers?: Fol
|
||||
}
|
||||
|
||||
let previousRegions: PreviousRegion[] = [];
|
||||
previousRegions.push({ indent: -1, line: model.getLineCount() + 1, marker: false }); // sentinel, to make sure there's at least one entry
|
||||
let line = model.getLineCount() + 1;
|
||||
previousRegions.push({ indent: -1, endAbove: line, line }); // sentinel, to make sure there's at least one entry
|
||||
|
||||
for (let line = model.getLineCount(); line > 0; line--) {
|
||||
let lineContent = model.getLineContent(line);
|
||||
let indent = TextModel.computeIndentLevel(lineContent, tabSize);
|
||||
let previous = previousRegions[previousRegions.length - 1];
|
||||
if (indent === -1) {
|
||||
if (offSide && !previous.marker) {
|
||||
// for offSide languages, empty lines are associated to the next block
|
||||
previous.line = line;
|
||||
if (offSide) {
|
||||
// for offSide languages, empty lines are associated to the previous block
|
||||
// note: the next block is already written to the results, so this only
|
||||
// impacts the end position of the block before
|
||||
previous.endAbove = line;
|
||||
}
|
||||
continue; // only whitespace
|
||||
}
|
||||
@@ -136,7 +143,7 @@ export function computeRanges(model: ITextModel, offSide: boolean, markers?: Fol
|
||||
if (m[1]) { // start pattern match
|
||||
// discard all regions until the folding pattern
|
||||
let i = previousRegions.length - 1;
|
||||
while (i > 0 && !previousRegions[i].marker) {
|
||||
while (i > 0 && previousRegions[i].indent !== -2) {
|
||||
i--;
|
||||
}
|
||||
if (i > 0) {
|
||||
@@ -145,15 +152,15 @@ export function computeRanges(model: ITextModel, offSide: boolean, markers?: Fol
|
||||
|
||||
// new folding range from pattern, includes the end line
|
||||
result.insertFirst(line, previous.line, indent);
|
||||
previous.marker = false;
|
||||
previous.indent = indent;
|
||||
previous.line = line;
|
||||
previous.indent = indent;
|
||||
previous.endAbove = line;
|
||||
continue;
|
||||
} else {
|
||||
// no end marker found, treat line as a regular line
|
||||
}
|
||||
} else { // end pattern match
|
||||
previousRegions.push({ indent: -2, line, marker: true });
|
||||
previousRegions.push({ indent: -2, endAbove: line, line });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -165,16 +172,16 @@ export function computeRanges(model: ITextModel, offSide: boolean, markers?: Fol
|
||||
} while (previous.indent > indent);
|
||||
|
||||
// new folding range
|
||||
let endLineNumber = previous.line - 1;
|
||||
let endLineNumber = previous.endAbove - 1;
|
||||
if (endLineNumber - line >= 1) { // needs at east size 1
|
||||
result.insertFirst(line, endLineNumber, indent);
|
||||
}
|
||||
}
|
||||
if (previous.indent === indent) {
|
||||
previous.line = line;
|
||||
previous.endAbove = line;
|
||||
} else { // previous.indent < indent
|
||||
// new region with a bigger indent
|
||||
previousRegions.push({ indent, line, marker: false });
|
||||
previousRegions.push({ indent, endAbove: line, line });
|
||||
}
|
||||
}
|
||||
return result.toIndentRanges(model);
|
||||
|
||||
@@ -316,5 +316,17 @@ suite('Folding with regions', () => {
|
||||
/* 8*/ '#endregionff',
|
||||
], [], true, markers);
|
||||
});
|
||||
|
||||
test('Issue 79359', () => {
|
||||
assertRanges([
|
||||
/* 1*/ '#region',
|
||||
/* 2*/ '',
|
||||
/* 3*/ 'class A',
|
||||
/* 4*/ ' foo',
|
||||
/* 5*/ '',
|
||||
/* 6*/ 'class A',
|
||||
/* 7*/ ' foo',
|
||||
/* 8*/ '',
|
||||
/* 9*/ '#endregion',
|
||||
], [r(1, 9, -1, true), r(3, 4, 0), r(6, 7, 0)], true, markers);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,6 +38,9 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { clearAllFontInfos } from 'vs/editor/browser/config/configuration';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IProductService } from 'vs/platform/product/common/product';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
@@ -51,7 +54,13 @@ function withAllStandaloneServices<T extends editorCommon.IEditor>(domElement: H
|
||||
}
|
||||
|
||||
if (!services.has(IOpenerService)) {
|
||||
services.set(IOpenerService, new OpenerService(services.get(ICodeEditorService), services.get(ICommandService)));
|
||||
services.set(IOpenerService, new OpenerService(
|
||||
services.get(ICodeEditorService),
|
||||
services.get(ICommandService),
|
||||
services.get(IStorageService),
|
||||
services.get(IDialogService),
|
||||
services.get(IProductService)
|
||||
));
|
||||
}
|
||||
|
||||
let result = callback(services);
|
||||
|
||||
@@ -27,11 +27,9 @@ function isArrayOf(elemType: (x: any) => boolean, obj: any): boolean {
|
||||
if (!(Array.isArray(obj))) {
|
||||
return false;
|
||||
}
|
||||
for (let idx in obj) {
|
||||
if (obj.hasOwnProperty(idx)) {
|
||||
if (!(elemType(obj[idx]))) {
|
||||
return false;
|
||||
}
|
||||
for (const el of obj) {
|
||||
if (!(elemType(el))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -298,10 +296,8 @@ function compileAction(lexer: monarchCommon.ILexerMin, ruleName: string, action:
|
||||
}
|
||||
else if (Array.isArray(action)) {
|
||||
let results: monarchCommon.FuzzyAction[] = [];
|
||||
for (let idx in action) {
|
||||
if (action.hasOwnProperty(idx)) {
|
||||
results[idx] = compileAction(lexer, ruleName, action[idx]);
|
||||
}
|
||||
for (let i = 0, len = action.length; i < len; i++) {
|
||||
results[i] = compileAction(lexer, ruleName, action[i]);
|
||||
}
|
||||
return { group: results };
|
||||
}
|
||||
@@ -331,13 +327,10 @@ function compileAction(lexer: monarchCommon.ILexerMin, ruleName: string, action:
|
||||
const def = lexer.defaultToken;
|
||||
return {
|
||||
test: function (id, matches, state, eos) {
|
||||
for (let idx in cases) {
|
||||
if (cases.hasOwnProperty(idx)) {
|
||||
const _case = cases[idx];
|
||||
const didmatch = (!_case.test || _case.test(id, matches, state, eos));
|
||||
if (didmatch) {
|
||||
return _case.value;
|
||||
}
|
||||
for (const _case of cases) {
|
||||
const didmatch = (!_case.test || _case.test(id, matches, state, eos));
|
||||
if (didmatch) {
|
||||
return _case.value;
|
||||
}
|
||||
}
|
||||
return def;
|
||||
@@ -425,64 +418,61 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm
|
||||
|
||||
// Compile an array of rules into newrules where RegExp objects are created.
|
||||
function addRules(state: string, newrules: monarchCommon.IRule[], rules: any[]) {
|
||||
for (let idx in rules) {
|
||||
if (rules.hasOwnProperty(idx)) {
|
||||
const rule = rules[idx];
|
||||
let include = rule.include;
|
||||
if (include) {
|
||||
if (typeof (include) !== 'string') {
|
||||
throw monarchCommon.createError(lexer, 'an \'include\' attribute must be a string at: ' + state);
|
||||
}
|
||||
if (include[0] === '@') {
|
||||
include = include.substr(1); // peel off starting @
|
||||
}
|
||||
if (!json.tokenizer[include]) {
|
||||
throw monarchCommon.createError(lexer, 'include target \'' + include + '\' is not defined at: ' + state);
|
||||
}
|
||||
addRules(state + '.' + include, newrules, json.tokenizer[include]);
|
||||
for (const rule of rules) {
|
||||
|
||||
let include = rule.include;
|
||||
if (include) {
|
||||
if (typeof (include) !== 'string') {
|
||||
throw monarchCommon.createError(lexer, 'an \'include\' attribute must be a string at: ' + state);
|
||||
}
|
||||
else {
|
||||
const newrule = new Rule(state);
|
||||
if (include[0] === '@') {
|
||||
include = include.substr(1); // peel off starting @
|
||||
}
|
||||
if (!json.tokenizer[include]) {
|
||||
throw monarchCommon.createError(lexer, 'include target \'' + include + '\' is not defined at: ' + state);
|
||||
}
|
||||
addRules(state + '.' + include, newrules, json.tokenizer[include]);
|
||||
}
|
||||
else {
|
||||
const newrule = new Rule(state);
|
||||
|
||||
|
||||
// Set up new rule attributes
|
||||
if (Array.isArray(rule) && rule.length >= 1 && rule.length <= 3) {
|
||||
newrule.setRegex(lexerMin, rule[0]);
|
||||
if (rule.length >= 3) {
|
||||
if (typeof (rule[1]) === 'string') {
|
||||
newrule.setAction(lexerMin, { token: rule[1], next: rule[2] });
|
||||
}
|
||||
else if (typeof (rule[1]) === 'object') {
|
||||
const rule1 = rule[1];
|
||||
rule1.next = rule[2];
|
||||
newrule.setAction(lexerMin, rule1);
|
||||
}
|
||||
else {
|
||||
throw monarchCommon.createError(lexer, 'a next state as the last element of a rule can only be given if the action is either an object or a string, at: ' + state);
|
||||
}
|
||||
// Set up new rule attributes
|
||||
if (Array.isArray(rule) && rule.length >= 1 && rule.length <= 3) {
|
||||
newrule.setRegex(lexerMin, rule[0]);
|
||||
if (rule.length >= 3) {
|
||||
if (typeof (rule[1]) === 'string') {
|
||||
newrule.setAction(lexerMin, { token: rule[1], next: rule[2] });
|
||||
}
|
||||
else if (typeof (rule[1]) === 'object') {
|
||||
const rule1 = rule[1];
|
||||
rule1.next = rule[2];
|
||||
newrule.setAction(lexerMin, rule1);
|
||||
}
|
||||
else {
|
||||
newrule.setAction(lexerMin, rule[1]);
|
||||
throw monarchCommon.createError(lexer, 'a next state as the last element of a rule can only be given if the action is either an object or a string, at: ' + state);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!rule.regex) {
|
||||
throw monarchCommon.createError(lexer, 'a rule must either be an array, or an object with a \'regex\' or \'include\' field at: ' + state);
|
||||
}
|
||||
if (rule.name) {
|
||||
if (typeof rule.name === 'string') {
|
||||
newrule.name = rule.name;
|
||||
}
|
||||
}
|
||||
if (rule.matchOnlyAtStart) {
|
||||
newrule.matchOnlyAtLineStart = bool(rule.matchOnlyAtLineStart, false);
|
||||
}
|
||||
newrule.setRegex(lexerMin, rule.regex);
|
||||
newrule.setAction(lexerMin, rule.action);
|
||||
newrule.setAction(lexerMin, rule[1]);
|
||||
}
|
||||
|
||||
newrules.push(newrule);
|
||||
}
|
||||
else {
|
||||
if (!rule.regex) {
|
||||
throw monarchCommon.createError(lexer, 'a rule must either be an array, or an object with a \'regex\' or \'include\' field at: ' + state);
|
||||
}
|
||||
if (rule.name) {
|
||||
if (typeof rule.name === 'string') {
|
||||
newrule.name = rule.name;
|
||||
}
|
||||
}
|
||||
if (rule.matchOnlyAtStart) {
|
||||
newrule.matchOnlyAtLineStart = bool(rule.matchOnlyAtLineStart, false);
|
||||
}
|
||||
newrule.setRegex(lexerMin, rule.regex);
|
||||
newrule.setAction(lexerMin, rule.action);
|
||||
}
|
||||
|
||||
newrules.push(newrule);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -520,26 +510,24 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm
|
||||
{ open: '<', close: '>', token: 'delimiter.angle' }];
|
||||
}
|
||||
let brackets: IMonarchLanguageBracket[] = [];
|
||||
for (let bracketIdx in json.brackets) {
|
||||
if (json.brackets.hasOwnProperty(bracketIdx)) {
|
||||
let desc = <any>json.brackets[bracketIdx];
|
||||
if (desc && Array.isArray(desc) && desc.length === 3) {
|
||||
desc = { token: desc[2], open: desc[0], close: desc[1] };
|
||||
}
|
||||
if (desc.open === desc.close) {
|
||||
throw monarchCommon.createError(lexer, 'open and close brackets in a \'brackets\' attribute must be different: ' + desc.open +
|
||||
'\n hint: use the \'bracket\' attribute if matching on equal brackets is required.');
|
||||
}
|
||||
if (typeof desc.open === 'string' && typeof desc.token === 'string' && typeof desc.close === 'string') {
|
||||
brackets.push({
|
||||
token: desc.token + lexer.tokenPostfix,
|
||||
open: monarchCommon.fixCase(lexer, desc.open),
|
||||
close: monarchCommon.fixCase(lexer, desc.close)
|
||||
});
|
||||
}
|
||||
else {
|
||||
throw monarchCommon.createError(lexer, 'every element in the \'brackets\' array must be a \'{open,close,token}\' object or array');
|
||||
}
|
||||
for (let el of json.brackets) {
|
||||
let desc: any = el;
|
||||
if (desc && Array.isArray(desc) && desc.length === 3) {
|
||||
desc = { token: desc[2], open: desc[0], close: desc[1] };
|
||||
}
|
||||
if (desc.open === desc.close) {
|
||||
throw monarchCommon.createError(lexer, 'open and close brackets in a \'brackets\' attribute must be different: ' + desc.open +
|
||||
'\n hint: use the \'bracket\' attribute if matching on equal brackets is required.');
|
||||
}
|
||||
if (typeof desc.open === 'string' && typeof desc.token === 'string' && typeof desc.close === 'string') {
|
||||
brackets.push({
|
||||
token: desc.token + lexer.tokenPostfix,
|
||||
open: monarchCommon.fixCase(lexer, desc.open),
|
||||
close: monarchCommon.fixCase(lexer, desc.close)
|
||||
});
|
||||
}
|
||||
else {
|
||||
throw monarchCommon.createError(lexer, 'every element in the \'brackets\' array must be a \'{open,close,token}\' object or array');
|
||||
}
|
||||
}
|
||||
lexer.brackets = brackets;
|
||||
|
||||
@@ -228,8 +228,6 @@ class MonarchLineState implements modes.IState {
|
||||
}
|
||||
}
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
|
||||
interface IMonarchTokensCollector {
|
||||
enterMode(startOffset: number, modeId: string): void;
|
||||
emit(startOffset: number, type: string): void;
|
||||
@@ -423,22 +421,24 @@ export class MonarchTokenizer implements modes.ITokenizationSupport {
|
||||
public getLoadStatus(): ILoadStatus {
|
||||
let promises: Thenable<any>[] = [];
|
||||
for (let nestedModeId in this._embeddedModes) {
|
||||
const tokenizationSupport = modes.TokenizationRegistry.get(nestedModeId);
|
||||
if (tokenizationSupport) {
|
||||
// The nested mode is already loaded
|
||||
if (tokenizationSupport instanceof MonarchTokenizer) {
|
||||
const nestedModeStatus = tokenizationSupport.getLoadStatus();
|
||||
if (nestedModeStatus.loaded === false) {
|
||||
promises.push(nestedModeStatus.promise);
|
||||
if (this._embeddedModes.hasOwnProperty(nestedModeId)) {
|
||||
const tokenizationSupport = modes.TokenizationRegistry.get(nestedModeId);
|
||||
if (tokenizationSupport) {
|
||||
// The nested mode is already loaded
|
||||
if (tokenizationSupport instanceof MonarchTokenizer) {
|
||||
const nestedModeStatus = tokenizationSupport.getLoadStatus();
|
||||
if (nestedModeStatus.loaded === false) {
|
||||
promises.push(nestedModeStatus.promise);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const tokenizationSupportPromise = modes.TokenizationRegistry.getPromise(nestedModeId);
|
||||
if (tokenizationSupportPromise) {
|
||||
// The nested mode is in the process of being loaded
|
||||
promises.push(tokenizationSupportPromise);
|
||||
const tokenizationSupportPromise = modes.TokenizationRegistry.getPromise(nestedModeId);
|
||||
if (tokenizationSupportPromise) {
|
||||
// The nested mode is in the process of being loaded
|
||||
promises.push(tokenizationSupportPromise);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,11 +490,7 @@ export class MonarchTokenizer implements modes.ITokenizationSupport {
|
||||
let popOffset = -1;
|
||||
let hasEmbeddedPopRule = false;
|
||||
|
||||
for (let idx in rules) {
|
||||
if (!hasOwnProperty.call(rules, idx)) {
|
||||
continue;
|
||||
}
|
||||
let rule: monarchCommon.IRule = rules[idx];
|
||||
for (const rule of rules) {
|
||||
if (!monarchCommon.isIAction(rule.action) || rule.action.nextEmbedded !== '@pop') {
|
||||
continue;
|
||||
}
|
||||
@@ -619,16 +615,13 @@ export class MonarchTokenizer implements modes.ITokenizationSupport {
|
||||
|
||||
// try each rule until we match
|
||||
let restOfLine = line.substr(pos);
|
||||
for (let idx in rules) {
|
||||
if (hasOwnProperty.call(rules, idx)) {
|
||||
let rule: monarchCommon.IRule = rules[idx];
|
||||
if (pos === 0 || !rule.matchOnlyAtLineStart) {
|
||||
matches = restOfLine.match(rule.regex);
|
||||
if (matches) {
|
||||
matched = matches[0];
|
||||
action = rule.action;
|
||||
break;
|
||||
}
|
||||
for (const rule of rules) {
|
||||
if (pos === 0 || !rule.matchOnlyAtLineStart) {
|
||||
matches = restOfLine.match(rule.regex);
|
||||
if (matches) {
|
||||
matched = matches[0];
|
||||
action = rule.action;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3998,8 +3998,12 @@ suite('autoClosingPairs', () => {
|
||||
{ open: '\'', close: '\'', notIn: ['string', 'comment'] },
|
||||
{ open: '\"', close: '\"', notIn: ['string'] },
|
||||
{ open: '`', close: '`', notIn: ['string', 'comment'] },
|
||||
{ open: '/**', close: ' */', notIn: ['string'] }
|
||||
{ open: '/**', close: ' */', notIn: ['string'] },
|
||||
{ open: 'begin', close: 'end', notIn: ['string'] }
|
||||
],
|
||||
__electricCharacterSupport: {
|
||||
docComment: { open: '/**', close: ' */' }
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -4439,6 +4443,28 @@ suite('autoClosingPairs', () => {
|
||||
mode.dispose();
|
||||
});
|
||||
|
||||
test('multi-character autoclose', () => {
|
||||
let mode = new AutoClosingMode();
|
||||
usingCursor({
|
||||
text: [
|
||||
'',
|
||||
],
|
||||
languageIdentifier: mode.getLanguageIdentifier()
|
||||
}, (model, cursor) => {
|
||||
|
||||
model.setValue('begi');
|
||||
cursor.setSelections('test', [new Selection(1, 5, 1, 5)]);
|
||||
cursorCommand(cursor, H.Type, { text: 'n' }, 'keyboard');
|
||||
assert.strictEqual(model.getLineContent(1), 'beginend');
|
||||
|
||||
model.setValue('/*');
|
||||
cursor.setSelections('test', [new Selection(1, 3, 1, 3)]);
|
||||
cursorCommand(cursor, H.Type, { text: '*' }, 'keyboard');
|
||||
assert.strictEqual(model.getLineContent(1), '/** */');
|
||||
});
|
||||
mode.dispose();
|
||||
});
|
||||
|
||||
test('issue #55314: Do not auto-close when ending with open', () => {
|
||||
const languageId = new LanguageIdentifier('myElectricMode', 5);
|
||||
class ElectricMode extends MockMode {
|
||||
@@ -4477,7 +4503,7 @@ suite('autoClosingPairs', () => {
|
||||
model.forceTokenization(model.getLineCount());
|
||||
assertType(model, cursor, 3, 4, '"', '"', `does not double quote when ending with open`);
|
||||
model.forceTokenization(model.getLineCount());
|
||||
assertType(model, cursor, 4, 2, '"', '""', `double quote when ending with open`);
|
||||
assertType(model, cursor, 4, 2, '"', '"', `does not double quote when ending with open`);
|
||||
model.forceTokenization(model.getLineCount());
|
||||
assertType(model, cursor, 4, 3, '"', '"', `does not double quote when ending with open`);
|
||||
});
|
||||
@@ -4772,31 +4798,18 @@ suite('autoClosingPairs', () => {
|
||||
|
||||
// on the mac US intl kb layout
|
||||
|
||||
// Typing ` + space
|
||||
// Typing ' + space
|
||||
cursorCommand(cursor, H.CompositionStart, null, 'keyboard');
|
||||
cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard');
|
||||
cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard');
|
||||
cursorCommand(cursor, H.CompositionEnd, null, 'keyboard');
|
||||
|
||||
assert.equal(model.getValue(), '\'\'');
|
||||
|
||||
// Typing " + space within string
|
||||
cursor.setSelections('test', [new Selection(1, 2, 1, 2)]);
|
||||
cursorCommand(cursor, H.CompositionStart, null, 'keyboard');
|
||||
cursorCommand(cursor, H.Type, { text: '"' }, 'keyboard');
|
||||
cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '"' }, 'keyboard');
|
||||
cursorCommand(cursor, H.CompositionEnd, null, 'keyboard');
|
||||
|
||||
assert.equal(model.getValue(), '\'"\'');
|
||||
|
||||
// Typing ' + space after '
|
||||
model.setValue('\'');
|
||||
cursor.setSelections('test', [new Selection(1, 2, 1, 2)]);
|
||||
// Typing one more ' + space
|
||||
cursorCommand(cursor, H.CompositionStart, null, 'keyboard');
|
||||
cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard');
|
||||
cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard');
|
||||
cursorCommand(cursor, H.CompositionEnd, null, 'keyboard');
|
||||
|
||||
assert.equal(model.getValue(), '\'\'');
|
||||
|
||||
// Typing ' as a closing tag
|
||||
|
||||
@@ -7,6 +7,10 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { OpenerService } from 'vs/editor/browser/services/openerService';
|
||||
import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices';
|
||||
import { CommandsRegistry, ICommandService, NullCommandService } from 'vs/platform/commands/common/commands';
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IProductService } from 'vs/platform/product/common/product';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
suite('OpenerService', function () {
|
||||
|
||||
@@ -24,19 +28,77 @@ suite('OpenerService', function () {
|
||||
}
|
||||
};
|
||||
|
||||
function getStorageService(trustedDomainsSetting: string[]) {
|
||||
let _settings = deepClone(trustedDomainsSetting);
|
||||
|
||||
return new class implements IStorageService {
|
||||
get = () => JSON.stringify(_settings);
|
||||
store = (key: string, val: string) => _settings = JSON.parse(val);
|
||||
|
||||
// Don't care
|
||||
_serviceBrand: any;
|
||||
|
||||
onDidChangeStorage = () => ({ dispose: () => { } });
|
||||
onWillSaveState = () => ({ dispose: () => { } });
|
||||
|
||||
getBoolean = () => true;
|
||||
getNumber = () => 0;
|
||||
remove = () => { };
|
||||
logStorage = () => { };
|
||||
};
|
||||
}
|
||||
|
||||
function getDialogService() {
|
||||
return new class implements IDialogService {
|
||||
_showInvoked = 0;
|
||||
show = () => {
|
||||
this._showInvoked++;
|
||||
return Promise.resolve({} as any);
|
||||
}
|
||||
get confirmInvoked() { return this._showInvoked; }
|
||||
|
||||
// Don't care
|
||||
_serviceBrand: any;
|
||||
confirm = () => {
|
||||
return Promise.resolve({} as any);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getProductService(): IProductService {
|
||||
return new class {
|
||||
nameShort: 'VS Code';
|
||||
|
||||
_serviceBrand: any;
|
||||
} as IProductService;
|
||||
}
|
||||
|
||||
|
||||
setup(function () {
|
||||
lastCommand = undefined;
|
||||
});
|
||||
|
||||
test('delegate to editorService, scheme:///fff', function () {
|
||||
const openerService = new OpenerService(editorService, NullCommandService);
|
||||
const openerService = new OpenerService(
|
||||
editorService,
|
||||
NullCommandService,
|
||||
getStorageService([]),
|
||||
getDialogService(),
|
||||
getProductService()
|
||||
);
|
||||
openerService.open(URI.parse('another:///somepath'));
|
||||
assert.equal(editorService.lastInput!.options!.selection, undefined);
|
||||
});
|
||||
|
||||
test('delegate to editorService, scheme:///fff#L123', function () {
|
||||
|
||||
const openerService = new OpenerService(editorService, NullCommandService);
|
||||
const openerService = new OpenerService(
|
||||
editorService,
|
||||
NullCommandService,
|
||||
getStorageService([]),
|
||||
getDialogService(),
|
||||
getProductService()
|
||||
);
|
||||
|
||||
openerService.open(URI.parse('file:///somepath#L23'));
|
||||
assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23);
|
||||
@@ -59,7 +121,13 @@ suite('OpenerService', function () {
|
||||
|
||||
test('delegate to editorService, scheme:///fff#123,123', function () {
|
||||
|
||||
const openerService = new OpenerService(editorService, NullCommandService);
|
||||
const openerService = new OpenerService(
|
||||
editorService,
|
||||
NullCommandService,
|
||||
getStorageService([]),
|
||||
getDialogService(),
|
||||
getProductService()
|
||||
);
|
||||
|
||||
openerService.open(URI.parse('file:///somepath#23'));
|
||||
assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23);
|
||||
@@ -78,7 +146,13 @@ suite('OpenerService', function () {
|
||||
|
||||
test('delegate to commandsService, command:someid', function () {
|
||||
|
||||
const openerService = new OpenerService(editorService, commandService);
|
||||
const openerService = new OpenerService(
|
||||
editorService,
|
||||
commandService,
|
||||
getStorageService([]),
|
||||
getDialogService(),
|
||||
getProductService()
|
||||
);
|
||||
|
||||
const id = `aCommand${Math.random()}`;
|
||||
CommandsRegistry.registerCommand(id, function () { });
|
||||
@@ -98,4 +172,70 @@ suite('OpenerService', function () {
|
||||
assert.equal(lastCommand!.args[0], 12);
|
||||
assert.equal(lastCommand!.args[1], true);
|
||||
});
|
||||
|
||||
test('links are protected by dialog.show', function () {
|
||||
const dialogService = getDialogService();
|
||||
const openerService = new OpenerService(
|
||||
editorService,
|
||||
commandService,
|
||||
getStorageService([]),
|
||||
dialogService,
|
||||
getProductService()
|
||||
);
|
||||
|
||||
openerService.open(URI.parse('https://www.microsoft.com'));
|
||||
assert.equal(dialogService.confirmInvoked, 1);
|
||||
});
|
||||
|
||||
test('links on the whitelisted domains can be opened without dialog.show', function () {
|
||||
const dialogService = getDialogService();
|
||||
const openerService = new OpenerService(
|
||||
editorService,
|
||||
commandService,
|
||||
getStorageService(['https://microsoft.com']),
|
||||
dialogService,
|
||||
getProductService()
|
||||
);
|
||||
|
||||
openerService.open(URI.parse('https://microsoft.com'));
|
||||
openerService.open(URI.parse('https://microsoft.com/'));
|
||||
openerService.open(URI.parse('https://microsoft.com/en-us/'));
|
||||
openerService.open(URI.parse('https://microsoft.com/en-us/?foo=bar'));
|
||||
openerService.open(URI.parse('https://microsoft.com/en-us/?foo=bar#baz'));
|
||||
|
||||
assert.equal(dialogService.confirmInvoked, 0);
|
||||
});
|
||||
|
||||
test('variations of links are protected by dialog confirmation', function () {
|
||||
const dialogService = getDialogService();
|
||||
const openerService = new OpenerService(
|
||||
editorService,
|
||||
commandService,
|
||||
getStorageService(['https://microsoft.com']),
|
||||
dialogService,
|
||||
getProductService()
|
||||
);
|
||||
|
||||
openerService.open(URI.parse('http://microsoft.com'));
|
||||
openerService.open(URI.parse('https://www.microsoft.com'));
|
||||
|
||||
assert.equal(dialogService.confirmInvoked, 2);
|
||||
});
|
||||
|
||||
test('* removes all link protection', function () {
|
||||
const dialogService = getDialogService();
|
||||
const openerService = new OpenerService(
|
||||
editorService,
|
||||
commandService,
|
||||
getStorageService(['*']),
|
||||
dialogService,
|
||||
getProductService()
|
||||
);
|
||||
|
||||
openerService.open(URI.parse('https://code.visualstudio.com/'));
|
||||
openerService.open(URI.parse('https://www.microsoft.com'));
|
||||
openerService.open(URI.parse('https://www.github.com'));
|
||||
|
||||
assert.equal(dialogService.confirmInvoked, 0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as assert from 'assert';
|
||||
import { StandardTokenType } from 'vs/editor/common/modes';
|
||||
import { CharacterPairSupport } from 'vs/editor/common/modes/supports/characterPair';
|
||||
import { TokenText, createFakeScopedLineTokens } from 'vs/editor/test/common/modesTestUtils';
|
||||
import { StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration';
|
||||
|
||||
suite('CharacterPairSupport', () => {
|
||||
|
||||
@@ -52,8 +53,21 @@ suite('CharacterPairSupport', () => {
|
||||
assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []);
|
||||
});
|
||||
|
||||
function findAutoClosingPair(characterPairSupport: CharacterPairSupport, character: string): StandardAutoClosingPairConditional | null {
|
||||
for (const autoClosingPair of characterPairSupport.getAutoClosingPairs()) {
|
||||
if (autoClosingPair.open === character) {
|
||||
return autoClosingPair;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function testShouldAutoClose(characterPairSupport: CharacterPairSupport, line: TokenText[], character: string, column: number): boolean {
|
||||
return characterPairSupport.shouldAutoClosePair(character, createFakeScopedLineTokens(line), column);
|
||||
const autoClosingPair = findAutoClosingPair(characterPairSupport, character);
|
||||
if (!autoClosingPair) {
|
||||
return false;
|
||||
}
|
||||
return CharacterPairSupport.shouldAutoClosePair(autoClosingPair, createFakeScopedLineTokens(line), column);
|
||||
}
|
||||
|
||||
test('shouldAutoClosePair in empty line', () => {
|
||||
|
||||
@@ -21,86 +21,20 @@ suite('Editor Modes - Auto Indentation', () => {
|
||||
assert.deepEqual(actual, null);
|
||||
}
|
||||
|
||||
function testAppends(electricCharacterSupport: BracketElectricCharacterSupport, line: TokenText[], character: string, offset: number, appendText: string): void {
|
||||
let actual = _testOnElectricCharacter(electricCharacterSupport, line, character, offset);
|
||||
assert.deepEqual(actual, { appendText: appendText });
|
||||
}
|
||||
|
||||
function testMatchBracket(electricCharacterSupport: BracketElectricCharacterSupport, line: TokenText[], character: string, offset: number, matchOpenBracket: string): void {
|
||||
let actual = _testOnElectricCharacter(electricCharacterSupport, line, character, offset);
|
||||
assert.deepEqual(actual, { matchOpenBracket: matchOpenBracket });
|
||||
}
|
||||
|
||||
test('Doc comments', () => {
|
||||
let brackets = new BracketElectricCharacterSupport(null, [{ open: '/**', close: ' */' }], null);
|
||||
|
||||
testAppends(brackets, [
|
||||
{ text: '/*', type: StandardTokenType.Other },
|
||||
], '*', 3, ' */');
|
||||
|
||||
testDoesNothing(brackets, [
|
||||
{ text: '/*', type: StandardTokenType.Other },
|
||||
{ text: ' ', type: StandardTokenType.Other },
|
||||
{ text: '*/', type: StandardTokenType.Other },
|
||||
], '*', 3);
|
||||
});
|
||||
|
||||
test('getElectricCharacters uses all sources and dedups', () => {
|
||||
let sup = new BracketElectricCharacterSupport(
|
||||
new RichEditBrackets(fakeLanguageIdentifier, [
|
||||
['{', '}'],
|
||||
['(', ')']
|
||||
]), [
|
||||
{ open: '{', close: '}', notIn: ['string', 'comment'] },
|
||||
{ open: '"', close: '"', notIn: ['string', 'comment'] },
|
||||
{ open: 'begin', close: 'end', notIn: ['string'] }
|
||||
],
|
||||
{ docComment: { open: '/**', close: ' */' } }
|
||||
])
|
||||
);
|
||||
|
||||
assert.deepEqual(sup.getElectricCharacters(), ['}', ')', 'n', '*']);
|
||||
});
|
||||
|
||||
test('auto-close', () => {
|
||||
let sup = new BracketElectricCharacterSupport(
|
||||
new RichEditBrackets(fakeLanguageIdentifier, [
|
||||
['{', '}'],
|
||||
['(', ')']
|
||||
]), [
|
||||
{ open: '{', close: '}', notIn: ['string', 'comment'] },
|
||||
{ open: '"', close: '"', notIn: ['string', 'comment'] },
|
||||
{ open: 'begin', close: 'end', notIn: ['string'] }
|
||||
],
|
||||
{ docComment: { open: '/**', close: ' */' } }
|
||||
);
|
||||
|
||||
testDoesNothing(sup, [], 'a', 0);
|
||||
|
||||
testDoesNothing(sup, [{ text: 'egi', type: StandardTokenType.Other }], 'b', 1);
|
||||
testDoesNothing(sup, [{ text: 'bgi', type: StandardTokenType.Other }], 'e', 2);
|
||||
testDoesNothing(sup, [{ text: 'bei', type: StandardTokenType.Other }], 'g', 3);
|
||||
testDoesNothing(sup, [{ text: 'beg', type: StandardTokenType.Other }], 'i', 4);
|
||||
|
||||
testDoesNothing(sup, [{ text: 'egin', type: StandardTokenType.Other }], 'b', 1);
|
||||
testDoesNothing(sup, [{ text: 'bgin', type: StandardTokenType.Other }], 'e', 2);
|
||||
testDoesNothing(sup, [{ text: 'bein', type: StandardTokenType.Other }], 'g', 3);
|
||||
testDoesNothing(sup, [{ text: 'begn', type: StandardTokenType.Other }], 'i', 4);
|
||||
testAppends(sup, [{ text: 'begi', type: StandardTokenType.Other }], 'n', 5, 'end');
|
||||
|
||||
testDoesNothing(sup, [{ text: '3gin', type: StandardTokenType.Other }], 'b', 1);
|
||||
testDoesNothing(sup, [{ text: 'bgin', type: StandardTokenType.Other }], '3', 2);
|
||||
testDoesNothing(sup, [{ text: 'b3in', type: StandardTokenType.Other }], 'g', 3);
|
||||
testDoesNothing(sup, [{ text: 'b3gn', type: StandardTokenType.Other }], 'i', 4);
|
||||
testDoesNothing(sup, [{ text: 'b3gi', type: StandardTokenType.Other }], 'n', 5);
|
||||
|
||||
testDoesNothing(sup, [{ text: 'begi', type: StandardTokenType.String }], 'n', 5);
|
||||
|
||||
testAppends(sup, [{ text: '"', type: StandardTokenType.String }, { text: 'begi', type: StandardTokenType.Other }], 'n', 6, 'end');
|
||||
testDoesNothing(sup, [{ text: '"', type: StandardTokenType.String }, { text: 'begi', type: StandardTokenType.String }], 'n', 6);
|
||||
|
||||
testAppends(sup, [{ text: '/*', type: StandardTokenType.String }], '*', 3, ' */');
|
||||
|
||||
testDoesNothing(sup, [{ text: 'begi', type: StandardTokenType.Other }, { text: 'end', type: StandardTokenType.Other }], 'n', 5);
|
||||
assert.deepEqual(sup.getElectricCharacters(), ['}', ')']);
|
||||
});
|
||||
|
||||
test('matchOpenBracket', () => {
|
||||
@@ -108,12 +42,7 @@ suite('Editor Modes - Auto Indentation', () => {
|
||||
new RichEditBrackets(fakeLanguageIdentifier, [
|
||||
['{', '}'],
|
||||
['(', ')']
|
||||
]), [
|
||||
{ open: '{', close: '}', notIn: ['string', 'comment'] },
|
||||
{ open: '"', close: '"', notIn: ['string', 'comment'] },
|
||||
{ open: 'begin', close: 'end', notIn: ['string'] }
|
||||
],
|
||||
{ docComment: { open: '/**', close: ' */' } }
|
||||
])
|
||||
);
|
||||
|
||||
testDoesNothing(sup, [{ text: '\t{', type: StandardTokenType.Other }], '\t', 1);
|
||||
|
||||
Reference in New Issue
Block a user