mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
69c87301aa
Treat link targets as file paths so crafted documentation cannot select arbitrary VS Code resource providers. Fixes https://github.com/angular/angular/issues/70512
246 lines
7.7 KiB
TypeScript
246 lines
7.7 KiB
TypeScript
/*!
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
import * as vscode from 'vscode';
|
|
import * as lsp from 'vscode-languageclient/node';
|
|
|
|
import {
|
|
OpenJsDocLinkCommand_Args,
|
|
OpenJsDocLinkCommandId,
|
|
ServerOptions,
|
|
} from '../../common/initialize';
|
|
|
|
import {AngularLanguageClient} from './client';
|
|
import {ANGULAR_SCHEME, TcbContentProvider} from './providers';
|
|
|
|
/**
|
|
* Represent a vscode command with an ID and an impl function `execute`.
|
|
*/
|
|
type Command<T = any> =
|
|
| {
|
|
id: string;
|
|
isTextEditorCommand: false;
|
|
execute(_: T): Promise<unknown>;
|
|
}
|
|
| {
|
|
id: string;
|
|
isTextEditorCommand: true;
|
|
execute(textEditor: vscode.TextEditor): Promise<unknown>;
|
|
};
|
|
|
|
/**
|
|
* Restart the language server by killing the process then spanwing a new one.
|
|
* @param client language client
|
|
* @param context extension context for adding disposables
|
|
*/
|
|
function restartNgServer(client: AngularLanguageClient): Command {
|
|
return {
|
|
id: 'angular.restartNgServer',
|
|
isTextEditorCommand: false,
|
|
async execute() {
|
|
await client.stop();
|
|
await client.start();
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Open the current server log file in a new editor.
|
|
*/
|
|
function openLogFile(client: AngularLanguageClient): Command {
|
|
return {
|
|
id: 'angular.openLogFile',
|
|
isTextEditorCommand: false,
|
|
async execute() {
|
|
const serverOptions: ServerOptions | undefined = client.initializeResult?.serverOptions;
|
|
if (!serverOptions?.logFile) {
|
|
// Show a MessageItem to help users automatically update the
|
|
// configuration option then restart the server.
|
|
const selection = await vscode.window.showErrorMessage(
|
|
`Angular server logging is off. Please set 'angular.log' and restart the server.`,
|
|
'Enable logging and restart server',
|
|
);
|
|
if (selection) {
|
|
const isGlobalConfig = false;
|
|
await vscode.workspace
|
|
.getConfiguration()
|
|
.update('angular.log', 'verbose', isGlobalConfig);
|
|
// Server will automatically restart because the config is changed
|
|
}
|
|
return;
|
|
}
|
|
const document = await vscode.workspace.openTextDocument(serverOptions.logFile);
|
|
return vscode.window.showTextDocument(document);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Command getTemplateTcb displays a typecheck block for the template a user has
|
|
* an active selection over, if any.
|
|
* @param ngClient LSP client for the active session
|
|
* @param context extension context to which disposables are pushed
|
|
*/
|
|
function getTemplateTcb(
|
|
ngClient: AngularLanguageClient,
|
|
context: vscode.ExtensionContext,
|
|
): Command {
|
|
const TCB_HIGHLIGHT_DECORATION = vscode.window.createTextEditorDecorationType({
|
|
// See https://code.visualstudio.com/api/references/theme-color#editor-colors
|
|
backgroundColor: new vscode.ThemeColor('editor.selectionHighlightBackground'),
|
|
});
|
|
|
|
const tcbProvider = new TcbContentProvider();
|
|
const disposable = vscode.workspace.registerTextDocumentContentProvider(
|
|
ANGULAR_SCHEME,
|
|
tcbProvider,
|
|
);
|
|
context.subscriptions.push(disposable);
|
|
|
|
return {
|
|
id: 'angular.getTemplateTcb',
|
|
isTextEditorCommand: true,
|
|
async execute(textEditor: vscode.TextEditor) {
|
|
tcbProvider.clear();
|
|
const response = await ngClient.getTcbUnderCursor(textEditor);
|
|
if (response === undefined) {
|
|
return undefined;
|
|
}
|
|
// Change the scheme of the URI from `file` to `ng` so that the document
|
|
// content is requested from our own `TcbContentProvider`.
|
|
const tcbUri = response.uri.with({
|
|
scheme: ANGULAR_SCHEME,
|
|
});
|
|
tcbProvider.update(tcbUri, response.content);
|
|
const editor = await vscode.window.showTextDocument(tcbUri, {
|
|
viewColumn: vscode.ViewColumn.Beside,
|
|
preserveFocus: true, // cursor remains in the active editor
|
|
});
|
|
editor.setDecorations(TCB_HIGHLIGHT_DECORATION, response.selections);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Command goToComponentWithTemplateFile finds components which reference an external template in
|
|
* their `templateUrl`s.
|
|
*
|
|
* @param ngClient LSP client for the active session
|
|
*/
|
|
function goToComponentWithTemplateFile(ngClient: AngularLanguageClient): Command {
|
|
return {
|
|
id: 'angular.goToComponentWithTemplateFile',
|
|
isTextEditorCommand: true,
|
|
async execute(textEditor: vscode.TextEditor) {
|
|
const locations = await ngClient.getComponentsForOpenExternalTemplate(textEditor);
|
|
if (locations === undefined) {
|
|
return;
|
|
}
|
|
|
|
vscode.commands.executeCommand(
|
|
'editor.action.goToLocations',
|
|
textEditor.document.uri,
|
|
textEditor.selection.active,
|
|
locations,
|
|
'peek' /** what to do when there are multiple results */,
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Command goToTemplateForComponent finds the template for a component.
|
|
*
|
|
* @param ngClient LSP client for the active session
|
|
*/
|
|
function goToTemplateForComponent(ngClient: AngularLanguageClient): Command {
|
|
return {
|
|
id: 'angular.goToTemplateForComponent',
|
|
isTextEditorCommand: true,
|
|
async execute(textEditor: vscode.TextEditor) {
|
|
const location = await ngClient.getTemplateLocationForComponent(textEditor);
|
|
if (location === null) {
|
|
return;
|
|
}
|
|
|
|
vscode.commands.executeCommand(
|
|
'editor.action.goToLocations',
|
|
textEditor.document.uri,
|
|
textEditor.selection.active,
|
|
[location],
|
|
'goto' /** What to do when there are multiple results (there can't be) */,
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Proxy command for opening links in jsdoc comments.
|
|
*
|
|
* This is needed to avoid VS Code incorrectly rewriting file names.
|
|
*/
|
|
function openJsDocLinkCommand(): Command<OpenJsDocLinkCommand_Args> {
|
|
return {
|
|
id: OpenJsDocLinkCommandId,
|
|
isTextEditorCommand: false,
|
|
async execute(args) {
|
|
if (typeof args?.file !== 'string' || args.file.length === 0) {
|
|
return;
|
|
}
|
|
// Command URI arguments are untrusted. This value is a filesystem path, so use `Uri.file`
|
|
// to prevent URI schemes from selecting another VS Code resource provider.
|
|
// https://code.visualstudio.com/api/references/vscode-api#Uri.file
|
|
const uri = vscode.Uri.file(args.file);
|
|
const document = await vscode.workspace.openTextDocument(uri);
|
|
return await vscode.window.showTextDocument(document, {
|
|
selection: new vscode.Range(
|
|
new vscode.Position(args.position?.start.line ?? 0, args.position?.start.character ?? 0),
|
|
new vscode.Position(args.position?.end.line ?? 0, args.position?.end.character ?? 0),
|
|
),
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
function applyCodeActionCommand(ngClient: AngularLanguageClient): Command {
|
|
return {
|
|
id: 'angular.applyCompletionCodeAction',
|
|
isTextEditorCommand: false,
|
|
async execute(args: lsp.WorkspaceEdit[]) {
|
|
await ngClient.applyWorkspaceEdits(args);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Register all supported vscode commands for the Angular extension.
|
|
* @param client language client
|
|
* @param context extension context for adding disposables
|
|
*/
|
|
export function registerCommands(
|
|
client: AngularLanguageClient,
|
|
context: vscode.ExtensionContext,
|
|
): void {
|
|
const commands: Command[] = [
|
|
restartNgServer(client),
|
|
openLogFile(client),
|
|
getTemplateTcb(client, context),
|
|
goToComponentWithTemplateFile(client),
|
|
goToTemplateForComponent(client),
|
|
openJsDocLinkCommand(),
|
|
applyCodeActionCommand(client),
|
|
];
|
|
|
|
for (const command of commands) {
|
|
const disposable = command.isTextEditorCommand
|
|
? vscode.commands.registerTextEditorCommand(command.id, command.execute)
|
|
: vscode.commands.registerCommand(command.id, command.execute);
|
|
context.subscriptions.push(disposable);
|
|
}
|
|
}
|