Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/languageserver/handlers/languageHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
TextDocumentPositionParams,
CodeLensParams,
DefinitionParams,
PrepareRenameParams,
RenameParams,
} from 'vscode-languageserver-protocol';
import {
CodeAction,
Expand All @@ -26,9 +28,11 @@ import {
DocumentSymbol,
Hover,
FoldingRange,
Range,
SelectionRange,
SymbolInformation,
TextEdit,
WorkspaceEdit,
} from 'vscode-languageserver-types';
import { isKubernetesAssociatedDocument } from '../../languageservice/parser/isKubernetes';
import { LanguageService } from '../../languageservice/yamlLanguageService';
Expand Down Expand Up @@ -70,6 +74,8 @@ export class LanguageHandlers {
this.connection.onCodeLens((params) => this.codeLensHandler(params));
this.connection.onCodeLensResolve((params) => this.codeLensResolveHandler(params));
this.connection.onDefinition((params) => this.definitionHandler(params));
this.connection.onPrepareRename((params) => this.prepareRenameHandler(params));
this.connection.onRenameRequest((params) => this.renameHandler(params));

this.yamlSettings.documents.onDidChangeContent((change) => this.cancelLimitExceededWarnings(change.document.uri));
this.yamlSettings.documents.onDidClose((event) => this.cancelLimitExceededWarnings(event.document.uri));
Expand Down Expand Up @@ -250,6 +256,24 @@ export class LanguageHandlers {
return this.languageService.doDefinition(textDocument, params);
}

prepareRenameHandler(params: PrepareRenameParams): Range | null {
const textDocument = this.yamlSettings.documents.get(params.textDocument.uri);
if (!textDocument) {
return null;
}

return this.languageService.prepareRename(textDocument, params);
}

renameHandler(params: RenameParams): WorkspaceEdit | null {
const textDocument = this.yamlSettings.documents.get(params.textDocument.uri);
if (!textDocument) {
return null;
}

return this.languageService.doRename(textDocument, params);
}

// Adapted from:
// https://github.com/microsoft/vscode/blob/94c9ea46838a9a619aeafb7e8afd1170c967bb55/extensions/json-language-features/server/src/jsonServer.ts#L172
private cancelLimitExceededWarnings(uri: string): void {
Expand Down
201 changes: 201 additions & 0 deletions src/languageservice/services/yamlRename.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Red Hat, Inc. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { TextDocument } from 'vscode-languageserver-textdocument';
import { Position, Range, WorkspaceEdit, TextEdit } from 'vscode-languageserver-types';
import { yamlDocumentsCache } from '../parser/yaml-documents';
import { matchOffsetToDocument } from '../utils/arrUtils';
import { TextBuffer } from '../utils/textBuffer';
import { Telemetry } from '../telemetry';
import { CST, isAlias, isCollection, isScalar, visit, Node } from 'yaml';
import { SourceToken, CollectionItem } from 'yaml/dist/parse/cst';
import { SingleYAMLDocument } from '../parser/yamlParser07';
import { isCollectionItem } from '../utils/astUtils';
import { PrepareRenameParams, RenameParams } from 'vscode-languageserver-protocol';

interface RenameTarget {
anchorNode: Node;
token: SourceToken;
yamlDoc: SingleYAMLDocument;
}

export class YamlRename {
constructor(private readonly telemetry?: Telemetry) {}

prepareRename(document: TextDocument, params: PrepareRenameParams): Range | null {
try {
const target = this.findTarget(document, params.position);
if (!target) {
return null;
}
if (!this.findAnchorToken(target.yamlDoc, target.anchorNode)) {
return null;
}
return this.getNameRange(document, target.token);
} catch (err) {
this.telemetry?.sendError('yaml.prepareRename.error', err);
return null;
}
}

doRename(document: TextDocument, params: RenameParams): WorkspaceEdit | null {
try {
const target = this.findTarget(document, params.position);
if (!target) {
return null;
}

const anchorToken = this.findAnchorToken(target.yamlDoc, target.anchorNode);
if (!anchorToken) {
return null;
}

const normalizedNewName = this.normalizeName(params.newName);
const edits: TextEdit[] = [];

edits.push(TextEdit.replace(this.getNameRange(document, anchorToken), normalizedNewName));

visit(target.yamlDoc.internalDocument, (key, node) => {
if (isAlias(node) && node.srcToken && node.resolve(target.yamlDoc.internalDocument) === target.anchorNode) {
edits.push(TextEdit.replace(this.getNameRange(document, node.srcToken as SourceToken), normalizedNewName));
}
});

if (edits.length === 0) {
return null;
}

return {
changes: {
[document.uri]: edits,
},
};
} catch (err) {
this.telemetry?.sendError('yaml.rename.error', err);
return null;
}
}

private findTarget(document: TextDocument, position: Position): RenameTarget | null {
const yamlDocuments = yamlDocumentsCache.getYamlDocument(document);
const offset = document.offsetAt(position);
const yamlDoc = matchOffsetToDocument(offset, yamlDocuments);
if (!yamlDoc) {
return null;
}

const [node] = yamlDoc.getNodeFromPosition(offset, new TextBuffer(document));
if (!node) {
return this.findByToken(yamlDoc, offset);
}

if (isAlias(node) && node.srcToken && this.isOffsetInsideToken(node.srcToken as SourceToken, offset)) {
const anchorNode = node.resolve(yamlDoc.internalDocument);
if (!anchorNode) {
return null;
}
return { anchorNode, token: node.srcToken as SourceToken, yamlDoc };
}

if ((isCollection(node) || isScalar(node)) && node.anchor) {
const anchorToken = this.findAnchorToken(yamlDoc, node);
if (anchorToken && this.isOffsetInsideToken(anchorToken, offset)) {
return { anchorNode: node, token: anchorToken, yamlDoc };
}
}

return this.findByToken(yamlDoc, offset);
}

private findByToken(yamlDoc: SingleYAMLDocument, offset: number): RenameTarget | null {
let target: RenameTarget;
visit(yamlDoc.internalDocument, (key, node) => {
if (isAlias(node) && node.srcToken && this.isOffsetInsideToken(node.srcToken as SourceToken, offset)) {
const anchorNode = node.resolve(yamlDoc.internalDocument);
if (anchorNode) {
target = { anchorNode, token: node.srcToken as SourceToken, yamlDoc };
return visit.BREAK;
}
}
if ((isCollection(node) || isScalar(node)) && node.anchor) {
const anchorToken = this.findAnchorToken(yamlDoc, node);
if (anchorToken && this.isOffsetInsideToken(anchorToken, offset)) {
target = { anchorNode: node, token: anchorToken, yamlDoc };
return visit.BREAK;
}
}
});

return target ?? null;
}

private findAnchorToken(yamlDoc: SingleYAMLDocument, node: Node): SourceToken | undefined {
const parent = yamlDoc.getParent(node);
const candidates = [];
if (parent && (parent as unknown as { srcToken?: SourceToken }).srcToken) {
candidates.push((parent as unknown as { srcToken: SourceToken }).srcToken);
}
if ((node as unknown as { srcToken?: SourceToken }).srcToken) {
candidates.push((node as unknown as { srcToken: SourceToken }).srcToken);
}

for (const token of candidates) {
const anchor = this.getAnchorFromToken(token, node);
if (anchor) {
return anchor;
}
}

return undefined;
}

private getAnchorFromToken(token: SourceToken, node: Node): SourceToken | undefined {
if (isCollectionItem(token)) {
return this.getAnchorFromCollectionItem(token);
} else if (CST.isCollection(token)) {
const collection = token as unknown as { items?: CollectionItem[] };
for (const item of collection.items ?? []) {
if (item.value !== (node as unknown as { srcToken?: SourceToken }).srcToken) {
continue;
}
const anchor = this.getAnchorFromCollectionItem(item);
if (anchor) {
return anchor;
}
}
}
return undefined;
}

private getAnchorFromCollectionItem(token: CollectionItem): SourceToken | undefined {
for (const t of token.start) {
if (t.type === 'anchor') {
return t;
}
}
if (token.sep && Array.isArray(token.sep)) {
for (const t of token.sep) {
if (t.type === 'anchor') {
return t;
}
}
}
return undefined;
}

private getNameRange(document: TextDocument, token: SourceToken): Range {
const startOffset = token.offset + 1;
const endOffset = token.offset + token.source.length;
return Range.create(document.positionAt(startOffset), document.positionAt(endOffset));
}

private isOffsetInsideToken(token: SourceToken, offset: number): boolean {
return offset >= token.offset && offset <= token.offset + token.source.length;
}

private normalizeName(name: string): string {
return name.replace(/^([*&])/, '');
}
}
10 changes: 10 additions & 0 deletions src/languageservice/yamlLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
CodeLens,
DefinitionLink,
SelectionRange,
Range,
WorkspaceEdit,
} from 'vscode-languageserver-types';
import { JSONSchema } from './jsonSchema';
import { YAMLDocumentSymbols } from './services/documentSymbols';
Expand All @@ -39,6 +41,8 @@ import {
Connection,
DocumentOnTypeFormattingParams,
DefinitionParams,
PrepareRenameParams,
RenameParams,
} from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { getFoldingRanges } from './services/yamlFolding';
Expand All @@ -54,6 +58,7 @@ import { SettingsState } from '../yamlSettings';
import { JSONSchemaSelection } from '../languageserver/handlers/schemaSelectionHandlers';
import { YamlDefinition } from './services/yamlDefinition';
import { getSelectionRanges } from './services/yamlSelectionRanges';
import { YamlRename } from './services/yamlRename';

export enum SchemaPriority {
SchemaStore = 1,
Expand Down Expand Up @@ -180,6 +185,8 @@ export interface LanguageService {
getCodeAction: (document: TextDocument, params: CodeActionParams) => CodeAction[] | undefined;
getCodeLens: (document: TextDocument) => PromiseLike<CodeLens[] | undefined> | CodeLens[] | undefined;
resolveCodeLens: (param: CodeLens) => PromiseLike<CodeLens> | CodeLens;
prepareRename: (document: TextDocument, params: PrepareRenameParams) => Range | null;
doRename: (document: TextDocument, params: RenameParams) => WorkspaceEdit | null;
}

export function getLanguageService(params: {
Expand All @@ -200,6 +207,7 @@ export function getLanguageService(params: {
const yamlCodeLens = new YamlCodeLens(schemaService, params.telemetry);
const yamlLinks = new YamlLinks(params.telemetry);
const yamlDefinition = new YamlDefinition(params.telemetry);
const yamlRename = new YamlRename(params.telemetry);

new JSONSchemaSelection(schemaService, params.yamlSettings, params.connection);

Expand Down Expand Up @@ -266,5 +274,7 @@ export function getLanguageService(params: {
return yamlCodeLens.getCodeLens(document);
},
resolveCodeLens: (param) => yamlCodeLens.resolveCodeLens(param),
prepareRename: (document, params) => yamlRename.prepareRename(document, params),
doRename: (document, params) => yamlRename.doRename(document, params),
};
}
1 change: 1 addition & 0 deletions src/yamlServerInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export class YAMLServerInit {
},
documentRangeFormattingProvider: false,
definitionProvider: true,
renameProvider: { prepareProvider: true },
documentLinkProvider: {},
foldingRangeProvider: true,
selectionRangeProvider: true,
Expand Down
Loading