From 368c03fdc8adbe3d155db44ee1200e4ebccf273c Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Wed, 20 Jan 2021 12:06:07 +0100 Subject: [PATCH] Fixes #114348: Allow `onEnterRules` to be defined in the language configuration file --- src/vs/vscode.proposed.d.ts | 2 +- .../languageConfigurationExtensionPoint.ts | 286 +++++++++++++++--- 2 files changed, 239 insertions(+), 49 deletions(-) diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 7d67b32ada9..bdf474c3abe 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -884,7 +884,7 @@ declare module 'vscode' { //#region https://github.com/microsoft/vscode/issues/58440 export interface OnEnterRule { /** - * This rule will only execute if the text above the this line matches this regular expression. + * This rule will only execute if the text above the line matches this regular expression. */ oneLineAboveText?: RegExp; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts index 69ee095045d..2b19a21c53b 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts @@ -9,7 +9,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { LanguageIdentifier } from 'vs/editor/common/modes'; -import { CharacterPair, CommentRule, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentationRule, LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration'; +import { CharacterPair, CommentRule, EnterAction, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentAction, IndentationRule, LanguageConfiguration, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { IModeService } from 'vs/editor/common/services/modeService'; import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; @@ -31,6 +31,19 @@ interface IIndentationRules { unIndentedLinePattern?: string | IRegExp; } +interface IEnterAction { + indent: 'none' | 'indent' | 'indentOutdent' | 'outdent'; + appendText?: string; + removeText?: number; +} + +interface IOnEnterRule { + beforeText: string | IRegExp; + afterText?: string | IRegExp; + oneLineAboveText?: string | IRegExp; + action: IEnterAction; +} + interface ILanguageConfiguration { comments?: CommentRule; brackets?: CharacterPair[]; @@ -40,6 +53,7 @@ interface ILanguageConfiguration { indentationRules?: IIndentationRules; folding?: FoldingRules; autoCloseBefore?: string; + onEnterRules?: IOnEnterRule[]; } function isStringArr(something: string[] | null): something is string[] { @@ -93,7 +107,7 @@ export class LanguageConfigurationFileHandler { } this._done[languageIdentifier.id] = true; - let configurationFiles = this._modeService.getConfigurationFiles(languageIdentifier.language); + const configurationFiles = this._modeService.getConfigurationFiles(languageIdentifier.language); configurationFiles.forEach((configFileLocation) => this._handleConfigFile(languageIdentifier, configFileLocation)); } @@ -254,18 +268,82 @@ export class LanguageConfigurationFileHandler { return result; } - // private _mapCharacterPairs(pairs: Array): IAutoClosingPairConditional[] { - // return pairs.map(pair => { - // if (Array.isArray(pair)) { - // return { open: pair[0], close: pair[1] }; - // } - // return pair; - // }); - // } + private _extractValidOnEnterRules(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): OnEnterRule[] | null { + const source = configuration.onEnterRules; + if (typeof source === 'undefined') { + return null; + } + if (!Array.isArray(source)) { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules\` to be an array.`); + return null; + } + + let result: OnEnterRule[] | null = null; + for (let i = 0, len = source.length; i < len; i++) { + const onEnterRule = source[i]; + if (!types.isObject(onEnterRule)) { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}]\` to be an object.`); + continue; + } + if (!types.isObject(onEnterRule.action)) { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action\` to be an object.`); + continue; + } + let indentAction: IndentAction; + if (onEnterRule.action.indent === 'none') { + indentAction = IndentAction.None; + } else if (onEnterRule.action.indent === 'indent') { + indentAction = IndentAction.Indent; + } else if (onEnterRule.action.indent === 'indentOutdent') { + indentAction = IndentAction.IndentOutdent; + } else if (onEnterRule.action.indent === 'outdent') { + indentAction = IndentAction.Outdent; + } else { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.indent\` to be 'none', 'indent', 'indentOutdent' or 'outdent'.`); + continue; + } + const action: EnterAction = { indentAction }; + if (onEnterRule.action.appendText) { + if (typeof onEnterRule.action.appendText === 'string') { + action.appendText = onEnterRule.action.appendText; + } else { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.appendText\` to be undefined or a string.`); + } + } + if (onEnterRule.action.removeText) { + if (typeof onEnterRule.action.removeText === 'number') { + action.removeText = onEnterRule.action.removeText; + } else { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.removeText\` to be undefined or a number.`); + } + } + const beforeText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].beforeText`, onEnterRule.beforeText); + if (!beforeText) { + continue; + } + const resultingOnEnterRule: OnEnterRule = { beforeText, action }; + if (onEnterRule.afterText) { + const afterText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].afterText`, onEnterRule.afterText); + if (afterText) { + resultingOnEnterRule.afterText = afterText; + } + } + if (onEnterRule.oneLineAboveText) { + const oneLineAboveText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].oneLineAboveText`, onEnterRule.oneLineAboveText); + if (oneLineAboveText) { + resultingOnEnterRule.oneLineAboveText = oneLineAboveText; + } + } + result = result || []; + result.push(resultingOnEnterRule); + } + + return result; + } private _handleConfig(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): void { - let richEditConfig: LanguageConfiguration = {}; + const richEditConfig: LanguageConfiguration = {}; const comments = this._extractValidCommentRule(languageIdentifier, configuration); if (comments) { @@ -293,25 +371,21 @@ export class LanguageConfigurationFileHandler { } if (configuration.wordPattern) { - try { - let wordPattern = this._parseRegex(configuration.wordPattern); - if (wordPattern) { - richEditConfig.wordPattern = wordPattern; - } - } catch (error) { - // Malformed regexes are ignored + const wordPattern = this._parseRegex(languageIdentifier, `wordPattern`, configuration.wordPattern); + if (wordPattern) { + richEditConfig.wordPattern = wordPattern; } } if (configuration.indentationRules) { - let indentationRules = this._mapIndentationRules(configuration.indentationRules); + const indentationRules = this._mapIndentationRules(languageIdentifier, configuration.indentationRules); if (indentationRules) { richEditConfig.indentationRules = indentationRules; } } if (configuration.folding) { - let markers = configuration.folding.markers; + const markers = configuration.folding.markers; richEditConfig.folding = { offSide: configuration.folding.offSide, @@ -319,44 +393,66 @@ export class LanguageConfigurationFileHandler { }; } + const onEnterRules = this._extractValidOnEnterRules(languageIdentifier, configuration); + if (onEnterRules) { + richEditConfig.onEnterRules = onEnterRules; + } + LanguageConfigurationRegistry.register(languageIdentifier, richEditConfig); } - private _parseRegex(value: string | IRegExp) { + private _parseRegex(languageIdentifier: LanguageIdentifier, confPath: string, value: string | IRegExp) { if (typeof value === 'string') { - return new RegExp(value, ''); - } else if (typeof value === 'object') { - return new RegExp(value.pattern, value.flags); + try { + return new RegExp(value, ''); + } catch (err) { + console.warn(`[${languageIdentifier.language}]: Invalid regular expression in \`${confPath}\`: `, err); + return null; + } } - + if (types.isObject(value)) { + if (typeof value.pattern !== 'string') { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}.pattern\` to be a string.`); + return null; + } + if (typeof value.flags !== 'undefined' && typeof value.flags !== 'string') { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}.flags\` to be a string.`); + return null; + } + try { + return new RegExp(value.pattern, value.flags); + } catch (err) { + console.warn(`[${languageIdentifier.language}]: Invalid regular expression in \`${confPath}\`: `, err); + return null; + } + } + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}\` to be a string or an object.`); return null; } - private _mapIndentationRules(indentationRules: IIndentationRules): IndentationRule | null { - try { - let increaseIndentPattern = this._parseRegex(indentationRules.increaseIndentPattern); - let decreaseIndentPattern = this._parseRegex(indentationRules.decreaseIndentPattern); - - if (increaseIndentPattern && decreaseIndentPattern) { - let result: IndentationRule = { - increaseIndentPattern: increaseIndentPattern, - decreaseIndentPattern: decreaseIndentPattern - }; - - if (indentationRules.indentNextLinePattern) { - result.indentNextLinePattern = this._parseRegex(indentationRules.indentNextLinePattern); - } - if (indentationRules.unIndentedLinePattern) { - result.unIndentedLinePattern = this._parseRegex(indentationRules.unIndentedLinePattern); - } - - return result; - } - } catch (error) { - // Malformed regexes are ignored + private _mapIndentationRules(languageIdentifier: LanguageIdentifier, indentationRules: IIndentationRules): IndentationRule | null { + const increaseIndentPattern = this._parseRegex(languageIdentifier, `indentationRules.increaseIndentPattern`, indentationRules.increaseIndentPattern); + if (!increaseIndentPattern) { + return null; + } + const decreaseIndentPattern = this._parseRegex(languageIdentifier, `indentationRules.decreaseIndentPattern`, indentationRules.decreaseIndentPattern); + if (!decreaseIndentPattern) { + return null; } - return null; + const result: IndentationRule = { + increaseIndentPattern: increaseIndentPattern, + decreaseIndentPattern: decreaseIndentPattern + }; + + if (indentationRules.indentNextLinePattern) { + result.indentNextLinePattern = this._parseRegex(languageIdentifier, `indentationRules.indentNextLinePattern`, indentationRules.indentNextLinePattern); + } + if (indentationRules.unIndentedLinePattern) { + result.unIndentedLinePattern = this._parseRegex(languageIdentifier, `indentationRules.unIndentedLinePattern`, indentationRules.unIndentedLinePattern); + } + + return result; } } @@ -601,6 +697,100 @@ const schema: IJSONSchema = { } } } + }, + onEnterRules: { + type: 'array', + items: { + type: 'object', + description: nls.localize('schema.onEnterRules', 'The language\'s rules to be evaluated when pressing Enter.'), + required: ['beforeText', 'action'], + properties: { + beforeText: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.beforeText', 'This rule will only execute if the text before the cursor matches this regular expression.'), + properties: { + pattern: { + type: 'string', + description: nls.localize('schema.onEnterRules.beforeText.pattern', 'The RegExp pattern for beforeText.'), + default: '', + }, + flags: { + type: 'string', + description: nls.localize('schema.onEnterRules.beforeText.flags', 'The RegExp flags for beforeText.'), + default: '', + pattern: '^([gimuy]+)$', + patternErrorMessage: nls.localize('schema.onEnterRules.beforeText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.') + } + } + }, + afterText: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.afterText', 'This rule will only execute if the text after the cursor matches this regular expression.'), + properties: { + pattern: { + type: 'string', + description: nls.localize('schema.onEnterRules.afterText.pattern', 'The RegExp pattern for afterText.'), + default: '', + }, + flags: { + type: 'string', + description: nls.localize('schema.onEnterRules.afterText.flags', 'The RegExp flags for afterText.'), + default: '', + pattern: '^([gimuy]+)$', + patternErrorMessage: nls.localize('schema.onEnterRules.afterText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.') + } + } + }, + oneLineAboveText: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.oneLineAboveText', 'This rule will only execute if the text above the line matches this regular expression.'), + properties: { + pattern: { + type: 'string', + description: nls.localize('schema.onEnterRules.oneLineAboveText.pattern', 'The RegExp pattern for oneLineAboveText.'), + default: '', + }, + flags: { + type: 'string', + description: nls.localize('schema.onEnterRules.oneLineAboveText.flags', 'The RegExp flags for oneLineAboveText.'), + default: '', + pattern: '^([gimuy]+)$', + patternErrorMessage: nls.localize('schema.onEnterRules.oneLineAboveText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.') + } + } + }, + action: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.action', 'The action to execute.'), + required: ['indent'], + default: { 'indent': 'indent' }, + properties: { + indent: { + type: 'string', + description: nls.localize('schema.onEnterRules.action.indent', "Describe what to do with the indentation"), + default: 'indent', + enum: ['none', 'indent', 'indentOutdent', 'outdent'], + markdownEnumDescriptions: [ + nls.localize('schema.onEnterRules.action.indent.none', "Insert new line and copy the previous line's indentation."), + nls.localize('schema.onEnterRules.action.indent.indent', "Insert new line and indent once (relative to the previous line's indentation)."), + nls.localize('schema.onEnterRules.action.indent.indentOutdent', "Insert two new lines:\n - the first one indented which will hold the cursor\n - the second one at the same indentation level"), + nls.localize('schema.onEnterRules.action.indent.outdent', "Insert new line and outdent once (relative to the previous line's indentation).") + ] + }, + appendText: { + type: 'string', + description: nls.localize('schema.onEnterRules.action.appendText', 'Describes text to be appended after the new line and after the indentation.'), + default: '', + }, + removeText: { + type: 'number', + description: nls.localize('schema.onEnterRules.action.removeText', 'Describes the number of characters to remove from the new line\'s indentation.'), + default: 0, + } + } + } + } + } } }