diff --git a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts index a482dda74119f..39bb4c6ae31f0 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts @@ -20,6 +20,7 @@ import { UriComponents } from './uri-components'; import { CompletionItemTag } from '../plugin/types-impl'; import { Event as TheiaEvent } from '@theia/core/lib/common/event'; import { URI } from '@theia/core/shared/vscode-uri'; +import { SerializedRegExp } from './plugin-api-rpc'; // Should contains internal Plugin API types @@ -541,6 +542,11 @@ export interface CallHierarchyOutgoingCall { fromRanges: Range[]; } +export interface LinkedEditingRanges { + ranges: Range[]; + wordPattern?: SerializedRegExp; +} + export interface SearchInWorkspaceResult { root: string; fileUri: string; diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 71e3c501fa40e..8595e8ae3eca9 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -71,7 +71,8 @@ import { CommentThreadCollapsibleState, CommentThread, CommentThreadChangedEvent, - CodeActionProviderDocumentation + CodeActionProviderDocumentation, + LinkedEditingRanges } from './plugin-api-rpc-model'; import { ExtPluginApi } from './plugin-ext-api-contribution'; import { KeysToAnyValues, KeysToKeysToAnyValue } from './types'; @@ -1483,6 +1484,7 @@ export interface LanguagesExt { $provideRootDefinition(handle: number, resource: UriComponents, location: Position, token: CancellationToken): Promise; $provideCallers(handle: number, definition: CallHierarchyItem, token: CancellationToken): Promise; $provideCallees(handle: number, definition: CallHierarchyItem, token: CancellationToken): Promise; + $provideLinkedEditingRanges(handle: number, resource: UriComponents, position: Position, token: CancellationToken): Promise; $releaseCallHierarchy(handle: number, session?: string): Promise; } @@ -1531,6 +1533,7 @@ export interface LanguagesMain { $emitDocumentSemanticTokensEvent(eventHandle: number): void; $registerDocumentRangeSemanticTokensProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], legend: theia.SemanticTokensLegend): void; $registerCallHierarchyProvider(handle: number, selector: SerializedDocumentFilter[]): void; + $registerLinkedEditingRangeProvider(handle: number, selector: SerializedDocumentFilter[]): void; } export interface WebviewInitData { diff --git a/packages/plugin-ext/src/main/browser/languages-main.ts b/packages/plugin-ext/src/main/browser/languages-main.ts index 8916d57887502..f4604850a3822 100644 --- a/packages/plugin-ext/src/main/browser/languages-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-main.ts @@ -948,6 +948,32 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable { }); } + // --- linked editing range + + $registerLinkedEditingRangeProvider(handle: number, selector: SerializedDocumentFilter[]): void { + const languageSelector = this.toLanguageSelector(selector); + const linkedEditingRangeProvider = this.createLinkedEditingRangeProvider(handle); + this.register(handle, + (monaco.languages.registerLinkedEditingRangeProvider as RegistrationFunction)(languageSelector, linkedEditingRangeProvider) + ); + } + + protected createLinkedEditingRangeProvider(handle: number): monaco.languages.LinkedEditingRangeProvider { + return { + provideLinkedEditingRanges: async (model: monaco.editor.ITextModel, position: monaco.Position, token: CancellationToken): + Promise => { + const res = await this.proxy.$provideLinkedEditingRanges(handle, model.uri, position, token); + if (res) { + return { + ranges: res.ranges, + wordPattern: reviveRegExp(res.wordPattern) + }; + } + return undefined; + } + }; + }; + } function reviveMarker(marker: MarkerData): vst.Diagnostic { diff --git a/packages/plugin-ext/src/plugin/languages-utils.ts b/packages/plugin-ext/src/plugin/languages-utils.ts new file mode 100644 index 0000000000000..b429e2f99f7d1 --- /dev/null +++ b/packages/plugin-ext/src/plugin/languages-utils.ts @@ -0,0 +1,55 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import * as theia from '@theia/plugin'; +import { SerializedIndentationRule, SerializedOnEnterRule, SerializedRegExp } from '../common'; + +export function serializeEnterRules(rules?: theia.OnEnterRule[]): SerializedOnEnterRule[] | undefined { + if (typeof rules === 'undefined' || rules === null) { + return undefined; + } + + return rules.map(r => + ({ + action: r.action, + beforeText: serializeRegExp(r.beforeText), + afterText: serializeRegExp(r.afterText) + } as SerializedOnEnterRule)); +} + +export function serializeRegExp(regexp?: RegExp): SerializedRegExp | undefined { + if (typeof regexp === 'undefined' || regexp === null) { + return undefined; + } + + return { + pattern: regexp.source, + flags: (regexp.global ? 'g' : '') + (regexp.ignoreCase ? 'i' : '') + (regexp.multiline ? 'm' : '') + }; +} + +export function serializeIndentation(indentationRules?: theia.IndentationRule): SerializedIndentationRule | undefined { + if (typeof indentationRules === 'undefined' || indentationRules === null) { + return undefined; + } + + return { + increaseIndentPattern: serializeRegExp(indentationRules.increaseIndentPattern), + decreaseIndentPattern: serializeRegExp(indentationRules.decreaseIndentPattern), + indentNextLinePattern: serializeRegExp(indentationRules.indentNextLinePattern), + unIndentedLinePattern: serializeRegExp(indentationRules.unIndentedLinePattern) + }; +} diff --git a/packages/plugin-ext/src/plugin/languages.ts b/packages/plugin-ext/src/plugin/languages.ts index f6ae2f1dfc206..9e99a84c144e9 100644 --- a/packages/plugin-ext/src/plugin/languages.ts +++ b/packages/plugin-ext/src/plugin/languages.ts @@ -19,9 +19,6 @@ import { PLUGIN_RPC_CONTEXT, LanguagesMain, SerializedLanguageConfiguration, - SerializedRegExp, - SerializedOnEnterRule, - SerializedIndentationRule, Position, Selection, RawColorInfo, @@ -63,6 +60,7 @@ import { CallHierarchyItem, CallHierarchyIncomingCall, CallHierarchyOutgoingCall, + LinkedEditingRanges, } from '../common/plugin-api-rpc-model'; import { CompletionAdapter } from './languages/completion'; import { Diagnostics } from './languages/diagnostics'; @@ -94,6 +92,8 @@ import { BinaryBuffer } from '@theia/core/lib/common/buffer'; import { DocumentSemanticTokensAdapter, DocumentRangeSemanticTokensAdapter } from './languages/semantic-highlighting'; import { isReadonlyArray } from '../common/arrays'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { LinkedEditingRangeAdapter } from './languages/linked-editing-range'; +import { serializeEnterRules, serializeIndentation, serializeRegExp } from './languages-utils'; type Adapter = CompletionAdapter | SignatureHelpAdapter | @@ -118,7 +118,8 @@ type Adapter = CompletionAdapter | RenameAdapter | CallHierarchyAdapter | DocumentRangeSemanticTokensAdapter | - DocumentSemanticTokensAdapter; + DocumentSemanticTokensAdapter | + LinkedEditingRangeAdapter; export class LanguagesExtImpl implements LanguagesExt { @@ -630,6 +631,19 @@ export class LanguagesExtImpl implements LanguagesExt { } // ### Call Hierarchy Provider end + // ### Linked Editing Range Provider begin + registerLinkedEditingRangeProvider(selector: theia.DocumentSelector, provider: theia.LinkedEditingRangeProvider): theia.Disposable { + const handle = this.addNewAdapter(new LinkedEditingRangeAdapter(this.documents, provider)); + this.proxy.$registerLinkedEditingRangeProvider(handle, this.transformDocumentSelector(selector)); + return this.createDisposable(handle); + } + + $provideLinkedEditingRanges(handle: number, resource: UriComponents, position: Position, token: theia.CancellationToken): Promise { + return this.withAdapter(handle, LinkedEditingRangeAdapter, async adapter => adapter.provideRanges(URI.revive(resource), position, token), undefined); + } + + // ### Linked Editing Range Provider end + // #region semantic coloring registerDocumentSemanticTokensProvider(selector: theia.DocumentSelector, provider: theia.DocumentSemanticTokensProvider, legend: theia.SemanticTokensLegend, @@ -671,43 +685,7 @@ export class LanguagesExtImpl implements LanguagesExt { // #endregion } -function serializeEnterRules(rules?: theia.OnEnterRule[]): SerializedOnEnterRule[] | undefined { - if (typeof rules === 'undefined' || rules === null) { - return undefined; - } - - return rules.map(r => - ({ - action: r.action, - beforeText: serializeRegExp(r.beforeText), - afterText: serializeRegExp(r.afterText) - } as SerializedOnEnterRule)); -} - -function serializeRegExp(regexp?: RegExp): SerializedRegExp | undefined { - if (typeof regexp === 'undefined' || regexp === null) { - return undefined; - } - - return { - pattern: regexp.source, - flags: (regexp.global ? 'g' : '') + (regexp.ignoreCase ? 'i' : '') + (regexp.multiline ? 'm' : '') - }; -} - -function serializeIndentation(indentationRules?: theia.IndentationRule): SerializedIndentationRule | undefined { - if (typeof indentationRules === 'undefined' || indentationRules === null) { - return undefined; - } - - return { - increaseIndentPattern: serializeRegExp(indentationRules.increaseIndentPattern), - decreaseIndentPattern: serializeRegExp(indentationRules.decreaseIndentPattern), - indentNextLinePattern: serializeRegExp(indentationRules.indentNextLinePattern), - unIndentedLinePattern: serializeRegExp(indentationRules.unIndentedLinePattern) - }; -} - function getPluginLabel(pluginInfo: PluginInfo): string { return pluginInfo.displayName || pluginInfo.name; } + diff --git a/packages/plugin-ext/src/plugin/languages/linked-editing-range.ts b/packages/plugin-ext/src/plugin/languages/linked-editing-range.ts new file mode 100644 index 0000000000000..3f7e853b5b901 --- /dev/null +++ b/packages/plugin-ext/src/plugin/languages/linked-editing-range.ts @@ -0,0 +1,48 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import * as theia from '@theia/plugin'; +import * as rpc from '../../common/plugin-api-rpc'; +import { DocumentsExtImpl } from '../documents'; +import { LinkedEditingRanges } from '../../common/plugin-api-rpc-model'; +import { URI } from '@theia/core/shared/vscode-uri'; +import { coalesce } from '../../common/arrays'; +import { fromRange, toPosition } from '../type-converters'; +import { serializeRegExp } from '../languages-utils'; + +export class LinkedEditingRangeAdapter { + + constructor( + private readonly documents: DocumentsExtImpl, + private readonly provider: theia.LinkedEditingRangeProvider + ) { } + + async provideRanges(resource: URI, position: rpc.Position, token: theia.CancellationToken): Promise { + + const doc = this.documents.getDocument(resource); + const pos = toPosition(position); + + const value = await this.provider.provideLinkedEditingRanges(doc, pos, token); + if (value && Array.isArray(value.ranges)) { + return { + ranges: coalesce(value.ranges.map(r => fromRange(r))), + wordPattern: serializeRegExp(value.wordPattern) + }; + } + return undefined; + } + +} diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index e9268d1c2c9b9..2fed256e795eb 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -140,7 +140,8 @@ import { SourceControlInputBoxValidationType, URI, FileDecoration, - ExtensionMode + ExtensionMode, + LinkedEditingRanges } from './types-impl'; import { AuthenticationExtImpl } from './authentication-ext'; import { SymbolKind } from '../common/plugin-api-rpc-model'; @@ -779,6 +780,9 @@ export function createAPIFactory( }, registerCallHierarchyProvider(selector: theia.DocumentSelector, provider: theia.CallHierarchyProvider): theia.Disposable { return languagesExt.registerCallHierarchyProvider(selector, provider); + }, + registerLinkedEditingRangeProvider(selector: theia.DocumentSelector, provider: theia.LinkedEditingRangeProvider): theia.Disposable { + return languagesExt.registerLinkedEditingRangeProvider(selector, provider); } }; @@ -1035,7 +1039,8 @@ export function createAPIFactory( SourceControlInputBoxValidationType, FileDecoration, CancellationError, - ExtensionMode + ExtensionMode, + LinkedEditingRanges }; }; } diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index efc37fc5d2246..1621250157da0 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -2486,6 +2486,18 @@ export class CallHierarchyOutgoingCall { } } +@es5ClassCompat +export class LinkedEditingRanges { + + ranges: theia.Range[]; + wordPattern?: RegExp; + + constructor(ranges: Range[], wordPattern?: RegExp) { + this.ranges = ranges; + this.wordPattern = wordPattern; + } +} + @es5ClassCompat export class TimelineItem { timestamp: number; diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 59175bdece292..69c77d516ffc1 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -9232,6 +9232,20 @@ export module '@theia/plugin' { * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerCallHierarchyProvider(selector: DocumentSelector, provider: CallHierarchyProvider): Disposable; + + /** + * Register a linked editing range provider. + * + * Multiple providers can be registered for a language. In that case providers are sorted + * by their {@link languages.match score} and the best-matching provider that has a result is used. Failure + * of the selected provider will cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A linked editing range provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. + */ + export function registerLinkedEditingRangeProvider(selector: DocumentSelector, provider: LinkedEditingRangeProvider): Disposable; + } /** @@ -11334,6 +11348,50 @@ export module '@theia/plugin' { provideCallHierarchyOutgoingCalls(item: CallHierarchyItem, token: CancellationToken): ProviderResult; } + /** + * Represents a list of ranges that can be edited together along with a word pattern to describe valid range contents. + */ + export class LinkedEditingRanges { + /** + * Create a new linked editing ranges object. + * + * @param ranges A list of ranges that can be edited together + * @param wordPattern An optional word pattern that describes valid contents for the given ranges + */ + constructor(ranges: Range[], wordPattern?: RegExp); + + /** + * A list of ranges that can be edited together. The ranges must have + * identical length and text content. The ranges cannot overlap. + */ + readonly ranges: Range[]; + + /** + * An optional word pattern that describes valid contents for the given ranges. + * If no pattern is provided, the language configuration's word pattern will be used. + */ + readonly wordPattern?: RegExp; + } + + /** + * The linked editing range provider interface defines the contract between extensions and + * the linked editing feature. + */ + export interface LinkedEditingRangeProvider { + /** + * For a given position in a document, returns the range of the symbol at the position and all ranges + * that have the same content. A change to one of the ranges can be applied to all other ranges if the new content + * is valid. An optional word pattern can be returned with the result to describe valid contents. + * If no result-specific word pattern is provided, the word pattern from the language configuration is used. + * + * @param document The document in which the provider was invoked. + * @param position The position at which the provider was invoked. + * @param token A cancellation token. + * @return A list of ranges that can be edited together + */ + provideLinkedEditingRanges(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + } + /** * Represents a session of a currently logged in user. */