mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 12:04:04 +01:00
Merge branch 'main' into ai-inferFromUsage
This commit is contained in:
@@ -10,6 +10,7 @@ export const file = 'file';
|
||||
export const untitled = 'untitled';
|
||||
export const git = 'git';
|
||||
export const github = 'github';
|
||||
export const azurerepos = 'azurerepos';
|
||||
|
||||
/** Live share scheme */
|
||||
export const vsls = 'vsls';
|
||||
@@ -39,4 +40,5 @@ export const disabledSchemes = new Set([
|
||||
git,
|
||||
vsls,
|
||||
github,
|
||||
azurerepos,
|
||||
]);
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Logger } from './logging/logger';
|
||||
import { getPackageInfo } from './utils/packageInfo';
|
||||
import { isWebAndHasSharedArrayBuffers } from './utils/platform';
|
||||
import { PluginManager } from './tsServer/plugins';
|
||||
import { Disposable } from './utils/dispose';
|
||||
|
||||
class StaticVersionProvider implements ITypeScriptVersionProvider {
|
||||
|
||||
@@ -78,7 +79,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
|
||||
logDirectoryProvider: noopLogDirectoryProvider,
|
||||
cancellerFactory: noopRequestCancellerFactory,
|
||||
versionProvider,
|
||||
processFactory: new WorkerServerProcessFactory(context.extensionUri),
|
||||
processFactory: new WorkerServerProcessFactory(context.extensionUri, logger),
|
||||
activeJsTsEditorTracker,
|
||||
serviceConfigurationProvider: new BrowserServiceConfigurationProvider(),
|
||||
experimentTelemetryReporter,
|
||||
@@ -96,13 +97,13 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
|
||||
});
|
||||
|
||||
context.subscriptions.push(lazilyActivateClient(lazyClientHost, pluginManager, activeJsTsEditorTracker, async () => {
|
||||
await preload(logger);
|
||||
await startPreloadWorkspaceContentsIfNeeded(context, logger);
|
||||
}));
|
||||
|
||||
return getExtensionApi(onCompletionAccepted.event, pluginManager);
|
||||
}
|
||||
|
||||
async function preload(logger: Logger): Promise<void> {
|
||||
async function startPreloadWorkspaceContentsIfNeeded(context: vscode.ExtensionContext, logger: Logger): Promise<void> {
|
||||
if (!isWebAndHasSharedArrayBuffers()) {
|
||||
return;
|
||||
}
|
||||
@@ -113,15 +114,46 @@ async function preload(logger: Logger): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const remoteHubApi = await RemoteRepositories.getApi();
|
||||
if (await remoteHubApi.loadWorkspaceContents?.(workspaceUri)) {
|
||||
logger.info(`Successfully loaded workspace content for repository ${workspaceUri.toString()}`);
|
||||
} else {
|
||||
logger.info(`Failed to load workspace content for repository ${workspaceUri.toString()}`);
|
||||
const loader = new RemoteWorkspaceContentsPreloader(workspaceUri, logger);
|
||||
context.subscriptions.push(loader);
|
||||
return loader.triggerPreload();
|
||||
}
|
||||
|
||||
class RemoteWorkspaceContentsPreloader extends Disposable {
|
||||
|
||||
private _preload: Promise<void> | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceUri: vscode.Uri,
|
||||
private readonly logger: Logger,
|
||||
) {
|
||||
super();
|
||||
|
||||
const fsWatcher = this._register(vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(workspaceUri, '*')));
|
||||
this._register(fsWatcher.onDidChange(uri => {
|
||||
if (uri.toString() === workspaceUri.toString()) {
|
||||
this._preload = undefined;
|
||||
this.triggerPreload();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async triggerPreload() {
|
||||
this._preload ??= this.doPreload();
|
||||
return this._preload;
|
||||
}
|
||||
|
||||
private async doPreload(): Promise<void> {
|
||||
try {
|
||||
const remoteHubApi = await RemoteRepositories.getApi();
|
||||
if (await remoteHubApi.loadWorkspaceContents?.(this.workspaceUri)) {
|
||||
this.logger.info(`Successfully loaded workspace content for repository ${this.workspaceUri.toString()}`);
|
||||
} else {
|
||||
this.logger.info(`Failed to load workspace content for repository ${this.workspaceUri.toString()}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.info(`Loading workspace content for repository ${this.workspaceUri.toString()} failed: ${error instanceof Error ? error.toString() : 'Unknown reason'}`);
|
||||
console.error(error);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.info(`Loading workspace content for repository ${workspaceUri.toString()} failed: ${error instanceof Error ? error.toString() : 'Unknown reason'}`);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ export class DiagnosticsManager extends Disposable {
|
||||
public override dispose() {
|
||||
super.dispose();
|
||||
|
||||
for (const value of this._pendingUpdates.values) {
|
||||
for (const value of this._pendingUpdates.values()) {
|
||||
clearTimeout(value);
|
||||
}
|
||||
this._pendingUpdates.clear();
|
||||
@@ -259,7 +259,7 @@ export class DiagnosticsManager extends Disposable {
|
||||
|
||||
private rebuildAll(): void {
|
||||
this._currentDiagnostics.clear();
|
||||
for (const fileDiagnostic of this._diagnostics.values) {
|
||||
for (const fileDiagnostic of this._diagnostics.values()) {
|
||||
this.rebuildFile(fileDiagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +195,7 @@ export default class FileConfigurationManager extends Disposable {
|
||||
allowIncompleteCompletions: true,
|
||||
displayPartsForJSDoc: true,
|
||||
disableLineTextInReferences: true,
|
||||
interactiveInlayHints: true,
|
||||
...getInlayHintsPreferences(config),
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DocumentSelector } from '../configuration/documentSelector';
|
||||
import { LanguageDescription } from '../configuration/languageDescription';
|
||||
import { API } from '../tsServer/api';
|
||||
import type * as Proto from '../tsServer/protocol/protocol';
|
||||
import { Position } from '../typeConverters';
|
||||
import { Location, Position } from '../typeConverters';
|
||||
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
|
||||
import { Disposable } from '../utils/dispose';
|
||||
import FileConfigurationManager, { InlayHintSettingNames, getInlayHintsPreferences } from './fileConfigurationManager';
|
||||
@@ -77,7 +77,7 @@ class TypeScriptInlayHintsProvider extends Disposable implements vscode.InlayHin
|
||||
return response.body.map(hint => {
|
||||
const result = new vscode.InlayHint(
|
||||
Position.fromLocation(hint.position),
|
||||
hint.text,
|
||||
this.convertInlayHintText(model.uri, hint),
|
||||
hint.kind && fromProtocolInlayHintKind(hint.kind)
|
||||
);
|
||||
result.paddingLeft = hint.whitespaceBefore;
|
||||
@@ -85,6 +85,21 @@ class TypeScriptInlayHintsProvider extends Disposable implements vscode.InlayHin
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private convertInlayHintText(resource: vscode.Uri, tsHint: Proto.InlayHintItem): string | vscode.InlayHintLabelPart[] {
|
||||
if (tsHint.displayParts) {
|
||||
return tsHint.displayParts.map((part): vscode.InlayHintLabelPart => {
|
||||
const out = new vscode.InlayHintLabelPart(part.text);
|
||||
if (part.span) {
|
||||
out.location = Location.fromTextSpan(resource, part.span);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
return tsHint.text;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function fromProtocolInlayHintKind(kind: Proto.InlayHintKind): vscode.InlayHintKind | undefined {
|
||||
|
||||
@@ -103,7 +103,7 @@ class JsDocCompletionProvider implements vscode.CompletionItemProvider {
|
||||
export function templateToSnippet(template: string): vscode.SnippetString {
|
||||
// TODO: use append placeholder
|
||||
let snippetIndex = 1;
|
||||
template = template.replace(/\$/g, '\\$');
|
||||
template = template.replace(/\$/g, '\\$'); // CodeQL [SM02383] This is only used for text which is put into the editor. It is not for rendered html
|
||||
template = template.replace(/^[ \t]*(?=(\/|[ ]\*))/gm, '');
|
||||
template = template.replace(/^(\/\*\*\s*\*[ ]*)$/m, (x) => x + `\$0`);
|
||||
template = template.replace(/\* @param([ ]\{\S+\})?\s+(\S+)[ \t]*$/gm, (_param, type, post) => {
|
||||
|
||||
@@ -175,47 +175,80 @@ class MoveToFileRefactorCommand implements Command {
|
||||
if (response.type !== 'response' || !response.body) {
|
||||
return;
|
||||
}
|
||||
const body = response.body;
|
||||
|
||||
const selectExistingFileItem: vscode.QuickPickItem = {
|
||||
label: vscode.l10n.t("Select existing file..."),
|
||||
};
|
||||
const selectNewFileItem: vscode.QuickPickItem = {
|
||||
label: vscode.l10n.t("Enter new file path..."),
|
||||
};
|
||||
|
||||
type DestinationItem = vscode.QuickPickItem & { readonly file: string };
|
||||
type DestinationItem = vscode.QuickPickItem & { readonly file?: string };
|
||||
const selectExistingFileItem: vscode.QuickPickItem = { label: vscode.l10n.t("Select existing file...") };
|
||||
const selectNewFileItem: vscode.QuickPickItem = { label: vscode.l10n.t("Enter new file path...") };
|
||||
|
||||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri);
|
||||
const destinationItems = response.body.files.map((file): DestinationItem => {
|
||||
const uri = this.client.toResource(file);
|
||||
const parentDir = Utils.dirname(uri);
|
||||
const quickPick = vscode.window.createQuickPick<DestinationItem>();
|
||||
quickPick.ignoreFocusOut = true;
|
||||
|
||||
let description;
|
||||
if (workspaceFolder) {
|
||||
if (uri.scheme === Schemes.file) {
|
||||
description = path.relative(workspaceFolder.uri.fsPath, parentDir.fsPath);
|
||||
} else {
|
||||
description = path.posix.relative(workspaceFolder.uri.path, parentDir.path);
|
||||
}
|
||||
} else {
|
||||
description = parentDir.fsPath;
|
||||
// true so we don't skip computing in the first call
|
||||
let quickPickInRelativeMode = true;
|
||||
const updateItems = () => {
|
||||
const relativeQuery = ['./', '../'].find(str => quickPick.value.startsWith(str));
|
||||
if (quickPickInRelativeMode === false && !!relativeQuery === false) {
|
||||
return;
|
||||
}
|
||||
quickPickInRelativeMode = !!relativeQuery;
|
||||
const destinationItems = body.files.map((file): DestinationItem | undefined => {
|
||||
const uri = this.client.toResource(file);
|
||||
const parentDir = Utils.dirname(uri);
|
||||
const filename = Utils.basename(uri);
|
||||
|
||||
return {
|
||||
file,
|
||||
label: Utils.basename(uri),
|
||||
description,
|
||||
};
|
||||
});
|
||||
let description: string | undefined;
|
||||
if (workspaceFolder) {
|
||||
if (uri.scheme === Schemes.file) {
|
||||
description = path.relative(workspaceFolder.uri.fsPath, parentDir.fsPath);
|
||||
} else {
|
||||
description = path.posix.relative(workspaceFolder.uri.path, parentDir.path);
|
||||
}
|
||||
if (relativeQuery) {
|
||||
const convertRelativePath = (str: string) => {
|
||||
return !str.startsWith('../') ? `./${str}` : str;
|
||||
};
|
||||
|
||||
const picked = await vscode.window.showQuickPick([
|
||||
selectExistingFileItem,
|
||||
selectNewFileItem,
|
||||
{ label: vscode.l10n.t("Destination Files"), kind: vscode.QuickPickItemKind.Separator },
|
||||
...destinationItems
|
||||
], {
|
||||
title: vscode.l10n.t("Move to File"),
|
||||
placeHolder: vscode.l10n.t("Select move destination"),
|
||||
const relativePath = convertRelativePath(path.relative(path.dirname(document.uri.fsPath), uri.fsPath));
|
||||
if (!relativePath.startsWith(relativeQuery)) {
|
||||
return;
|
||||
}
|
||||
description = relativePath;
|
||||
}
|
||||
} else {
|
||||
description = parentDir.fsPath;
|
||||
}
|
||||
|
||||
return {
|
||||
file,
|
||||
label: Utils.basename(uri),
|
||||
description: relativeQuery ? description : path.join(description, filename),
|
||||
};
|
||||
});
|
||||
quickPick.items = [
|
||||
selectExistingFileItem,
|
||||
selectNewFileItem,
|
||||
{ label: vscode.l10n.t("Destination Files"), kind: vscode.QuickPickItemKind.Separator },
|
||||
...coalesce(destinationItems)
|
||||
];
|
||||
};
|
||||
quickPick.title = vscode.l10n.t("Move to File");
|
||||
quickPick.placeholder = vscode.l10n.t("Enter file path");
|
||||
quickPick.matchOnDescription = true;
|
||||
quickPick.onDidChangeValue(updateItems);
|
||||
updateItems();
|
||||
|
||||
const picked = await new Promise<DestinationItem | undefined>(resolve => {
|
||||
quickPick.onDidAccept(() => {
|
||||
resolve(quickPick.selectedItems[0]);
|
||||
quickPick.dispose();
|
||||
});
|
||||
quickPick.onDidHide(() => {
|
||||
resolve(undefined);
|
||||
quickPick.dispose();
|
||||
});
|
||||
quickPick.show();
|
||||
});
|
||||
if (!picked) {
|
||||
return;
|
||||
@@ -236,7 +269,7 @@ class MoveToFileRefactorCommand implements Command {
|
||||
});
|
||||
return picked ? this.client.toTsFilePath(picked) : undefined;
|
||||
} else {
|
||||
return (picked as DestinationItem).file;
|
||||
return picked.file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as jsonc from 'jsonc-parser';
|
||||
import { basename, posix } from 'path';
|
||||
import { posix } from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { Utils } from 'vscode-uri';
|
||||
import { coalesce } from '../utils/arrays';
|
||||
@@ -42,19 +42,31 @@ class TsconfigLinkProvider implements vscode.DocumentLinkProvider {
|
||||
}
|
||||
|
||||
private getExtendsLink(document: vscode.TextDocument, root: jsonc.Node): vscode.DocumentLink | undefined {
|
||||
const extendsNode = jsonc.findNodeAtLocation(root, ['extends']);
|
||||
if (!this.isPathValue(extendsNode)) {
|
||||
const node = jsonc.findNodeAtLocation(root, ['extends']);
|
||||
return node && this.tryCreateTsConfigLink(document, node);
|
||||
}
|
||||
|
||||
private getReferencesLinks(document: vscode.TextDocument, root: jsonc.Node) {
|
||||
return mapChildren(
|
||||
jsonc.findNodeAtLocation(root, ['references']),
|
||||
child => {
|
||||
const pathNode = jsonc.findNodeAtLocation(child, ['path']);
|
||||
return pathNode && this.tryCreateTsConfigLink(document, pathNode);
|
||||
});
|
||||
}
|
||||
|
||||
private tryCreateTsConfigLink(document: vscode.TextDocument, node: jsonc.Node): vscode.DocumentLink | undefined {
|
||||
if (!this.isPathValue(node)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const extendsValue: string = extendsNode.value;
|
||||
const args: OpenExtendsLinkCommandArgs = {
|
||||
resourceUri: { ...document.uri.toJSON(), $mid: undefined }, // Prevent VS Code from trying to transform the uri
|
||||
extendsValue: extendsValue
|
||||
resourceUri: { ...document.uri.toJSON(), $mid: undefined },
|
||||
extendsValue: node.value
|
||||
};
|
||||
|
||||
const link = new vscode.DocumentLink(
|
||||
this.getRange(document, extendsNode),
|
||||
this.getRange(document, node),
|
||||
vscode.Uri.parse(`command:${openExtendsLinkCommandId}?${JSON.stringify(args)}`));
|
||||
link.tooltip = vscode.l10n.t("Follow link");
|
||||
return link;
|
||||
@@ -66,22 +78,6 @@ class TsconfigLinkProvider implements vscode.DocumentLinkProvider {
|
||||
child => this.pathNodeToLink(document, child));
|
||||
}
|
||||
|
||||
private getReferencesLinks(document: vscode.TextDocument, root: jsonc.Node) {
|
||||
return mapChildren(
|
||||
jsonc.findNodeAtLocation(root, ['references']),
|
||||
child => {
|
||||
const pathNode = jsonc.findNodeAtLocation(child, ['path']);
|
||||
if (!this.isPathValue(pathNode)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new vscode.DocumentLink(this.getRange(document, pathNode),
|
||||
basename(pathNode.value).endsWith('.json')
|
||||
? this.getFileTarget(document, pathNode)
|
||||
: this.getFolderTarget(document, pathNode));
|
||||
});
|
||||
}
|
||||
|
||||
private pathNodeToLink(
|
||||
document: vscode.TextDocument,
|
||||
node: jsonc.Node | undefined
|
||||
@@ -91,21 +87,17 @@ class TsconfigLinkProvider implements vscode.DocumentLinkProvider {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private isPathValue(extendsNode: jsonc.Node | undefined): extendsNode is jsonc.Node {
|
||||
return extendsNode
|
||||
&& extendsNode.type === 'string'
|
||||
&& extendsNode.value
|
||||
&& !(extendsNode.value as string).includes('*'); // don't treat globs as links.
|
||||
private isPathValue(node: jsonc.Node | undefined): node is jsonc.Node {
|
||||
return node
|
||||
&& node.type === 'string'
|
||||
&& node.value
|
||||
&& !(node.value as string).includes('*'); // don't treat globs as links.
|
||||
}
|
||||
|
||||
private getFileTarget(document: vscode.TextDocument, node: jsonc.Node): vscode.Uri {
|
||||
return vscode.Uri.joinPath(Utils.dirname(document.uri), node.value);
|
||||
}
|
||||
|
||||
private getFolderTarget(document: vscode.TextDocument, node: jsonc.Node): vscode.Uri {
|
||||
return vscode.Uri.joinPath(Utils.dirname(document.uri), node.value, 'tsconfig.json');
|
||||
}
|
||||
|
||||
private getRange(document: vscode.TextDocument, node: jsonc.Node) {
|
||||
const offset = node.offset;
|
||||
const start = document.positionAt(offset + 1);
|
||||
@@ -156,7 +148,7 @@ async function resolveNodeModulesPath(baseDirUri: vscode.Uri, pathCandidates: st
|
||||
/**
|
||||
* @returns Returns undefined in case of lack of result while trying to resolve from node_modules
|
||||
*/
|
||||
async function getTsconfigPath(baseDirUri: vscode.Uri, extendsValue: string): Promise<vscode.Uri | undefined> {
|
||||
async function getTsconfigPath(baseDirUri: vscode.Uri, pathValue: string): Promise<vscode.Uri | undefined> {
|
||||
async function resolve(absolutePath: vscode.Uri): Promise<vscode.Uri> {
|
||||
if (absolutePath.path.endsWith('.json') || await exists(absolutePath)) {
|
||||
return absolutePath;
|
||||
@@ -166,21 +158,21 @@ async function getTsconfigPath(baseDirUri: vscode.Uri, extendsValue: string): Pr
|
||||
});
|
||||
}
|
||||
|
||||
const isRelativePath = ['./', '../'].some(str => extendsValue.startsWith(str));
|
||||
const isRelativePath = ['./', '../'].some(str => pathValue.startsWith(str));
|
||||
if (isRelativePath) {
|
||||
return resolve(vscode.Uri.joinPath(baseDirUri, extendsValue));
|
||||
return resolve(vscode.Uri.joinPath(baseDirUri, pathValue));
|
||||
}
|
||||
|
||||
if (extendsValue.startsWith('/') || looksLikeAbsoluteWindowsPath(extendsValue)) {
|
||||
return resolve(vscode.Uri.file(extendsValue));
|
||||
if (pathValue.startsWith('/') || looksLikeAbsoluteWindowsPath(pathValue)) {
|
||||
return resolve(vscode.Uri.file(pathValue));
|
||||
}
|
||||
|
||||
// Otherwise resolve like a module
|
||||
return resolveNodeModulesPath(baseDirUri, [
|
||||
extendsValue,
|
||||
...extendsValue.endsWith('.json') ? [] : [
|
||||
`${extendsValue}.json`,
|
||||
`${extendsValue}/tsconfig.json`,
|
||||
pathValue,
|
||||
...pathValue.endsWith('.json') ? [] : [
|
||||
`${pathValue}.json`,
|
||||
`${pathValue}/tsconfig.json`,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -49,9 +49,13 @@ function getTagBodyText(
|
||||
return '```\n' + text + '\n```';
|
||||
}
|
||||
|
||||
const text = convertLinkTags(tag.text, filePathConverter);
|
||||
let text = convertLinkTags(tag.text, filePathConverter);
|
||||
switch (tag.name) {
|
||||
case 'example': {
|
||||
// Example text does not support `{@link}` as it is considered code.
|
||||
// TODO: should we support it if it appears outside of an explicit code block?
|
||||
text = asPlainText(tag.text);
|
||||
|
||||
// check for caption tags, fix for #79704
|
||||
const captionTagMatches = text.match(/<caption>(.*?)<\/caption>\s*(\r\n|\n)/);
|
||||
if (captionTagMatches && captionTagMatches.index === 0) {
|
||||
@@ -132,6 +136,13 @@ function getTagBody(tag: Proto.JSDocTagInfo, filePathConverter: IFilePathToResou
|
||||
return (convertLinkTags(tag.text, filePathConverter)).split(/^(\S+)\s*-?\s*/);
|
||||
}
|
||||
|
||||
function asPlainText(parts: readonly Proto.SymbolDisplayPart[] | string): string {
|
||||
if (typeof parts === 'string') {
|
||||
return parts;
|
||||
}
|
||||
return parts.map(part => part.text).join('');
|
||||
}
|
||||
|
||||
export function asPlainTextWithLinks(
|
||||
parts: readonly Proto.SymbolDisplayPart[] | string,
|
||||
filePathConverter: IFilePathToResourceConverter,
|
||||
@@ -177,10 +188,10 @@ function convertLinkTags(
|
||||
if (/^https?:/.test(text)) {
|
||||
const parts = text.split(' ');
|
||||
if (parts.length === 1) {
|
||||
out.push(parts[0]);
|
||||
out.push(`<${parts[0]}>`);
|
||||
} else if (parts.length > 1) {
|
||||
const linkText = escapeMarkdownSyntaxTokensForCode(parts.slice(1).join(' '));
|
||||
out.push(`[${currentLink.linkcode ? '`' + linkText + '`' : linkText}](${parts[0]})`);
|
||||
const linkText = parts.slice(1).join(' ');
|
||||
out.push(`[${currentLink.linkcode ? '`' + escapeMarkdownSyntaxTokensForCode(linkText) + '`' : linkText}](${parts[0]})`);
|
||||
}
|
||||
} else {
|
||||
out.push(escapeMarkdownSyntaxTokensForCode(text));
|
||||
@@ -217,7 +228,7 @@ function convertLinkTags(
|
||||
}
|
||||
|
||||
function escapeMarkdownSyntaxTokensForCode(text: string): string {
|
||||
return text.replace(/`/g, '\\$&');
|
||||
return text.replace(/`/g, '\\$&'); // CodeQL [SM02383] This is only meant to escape backticks. The Markdown is fully sanitized after being rendered.
|
||||
}
|
||||
|
||||
export function tagsToMarkdown(
|
||||
|
||||
@@ -14,7 +14,7 @@ const noopToResource: IFilePathToResourceConverter = {
|
||||
};
|
||||
|
||||
suite('typescript.previewer', () => {
|
||||
test('Should ignore hyphens after a param tag', async () => {
|
||||
test('Should ignore hyphens after a param tag', () => {
|
||||
assert.strictEqual(
|
||||
tagsToMarkdown([
|
||||
{
|
||||
@@ -25,7 +25,7 @@ suite('typescript.previewer', () => {
|
||||
'*@param* `a` — b');
|
||||
});
|
||||
|
||||
test('Should parse url jsdoc @link', async () => {
|
||||
test('Should parse url jsdoc @link', () => {
|
||||
assert.strictEqual(
|
||||
documentationToMarkdown(
|
||||
'x {@link http://www.example.com/foo} y {@link https://api.jquery.com/bind/#bind-eventType-eventData-handler} z',
|
||||
@@ -35,7 +35,7 @@ suite('typescript.previewer', () => {
|
||||
'x [http://www.example.com/foo](http://www.example.com/foo) y [https://api.jquery.com/bind/#bind-eventType-eventData-handler](https://api.jquery.com/bind/#bind-eventType-eventData-handler) z');
|
||||
});
|
||||
|
||||
test('Should parse url jsdoc @link with text', async () => {
|
||||
test('Should parse url jsdoc @link with text', () => {
|
||||
assert.strictEqual(
|
||||
documentationToMarkdown(
|
||||
'x {@link http://www.example.com/foo abc xyz} y {@link http://www.example.com/bar|b a z} z',
|
||||
@@ -45,7 +45,7 @@ suite('typescript.previewer', () => {
|
||||
'x [abc xyz](http://www.example.com/foo) y [b a z](http://www.example.com/bar) z');
|
||||
});
|
||||
|
||||
test('Should treat @linkcode jsdocs links as monospace', async () => {
|
||||
test('Should treat @linkcode jsdocs links as monospace', () => {
|
||||
assert.strictEqual(
|
||||
documentationToMarkdown(
|
||||
'x {@linkcode http://www.example.com/foo} y {@linkplain http://www.example.com/bar} z',
|
||||
@@ -55,7 +55,7 @@ suite('typescript.previewer', () => {
|
||||
'x [`http://www.example.com/foo`](http://www.example.com/foo) y [http://www.example.com/bar](http://www.example.com/bar) z');
|
||||
});
|
||||
|
||||
test('Should parse url jsdoc @link in param tag', async () => {
|
||||
test('Should parse url jsdoc @link in param tag', () => {
|
||||
assert.strictEqual(
|
||||
tagsToMarkdown([
|
||||
{
|
||||
@@ -66,7 +66,7 @@ suite('typescript.previewer', () => {
|
||||
'*@param* `a` — x [abc xyz](http://www.example.com/foo) y [b a z](http://www.example.com/bar) z');
|
||||
});
|
||||
|
||||
test('Should ignore unclosed jsdocs @link', async () => {
|
||||
test('Should ignore unclosed jsdocs @link', () => {
|
||||
assert.strictEqual(
|
||||
documentationToMarkdown(
|
||||
'x {@link http://www.example.com/foo y {@link http://www.example.com/bar bar} z',
|
||||
@@ -76,7 +76,7 @@ suite('typescript.previewer', () => {
|
||||
'x {@link http://www.example.com/foo y [bar](http://www.example.com/bar) z');
|
||||
});
|
||||
|
||||
test('Should support non-ascii characters in parameter name (#90108)', async () => {
|
||||
test('Should support non-ascii characters in parameter name (#90108)', () => {
|
||||
assert.strictEqual(
|
||||
tagsToMarkdown([
|
||||
{
|
||||
@@ -135,7 +135,35 @@ suite('typescript.previewer', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Should render @linkcode symbol name as code', async () => {
|
||||
test('Should not render @link inside of @example #187768', () => {
|
||||
assert.strictEqual(
|
||||
tagsToMarkdown([
|
||||
{
|
||||
"name": "example",
|
||||
"text": [
|
||||
{
|
||||
"text": "1 + 1 ",
|
||||
"kind": "text"
|
||||
},
|
||||
{
|
||||
"text": "{@link ",
|
||||
"kind": "link"
|
||||
},
|
||||
{
|
||||
"text": "foo",
|
||||
"kind": "linkName"
|
||||
},
|
||||
{
|
||||
"text": "}",
|
||||
"kind": "link"
|
||||
}
|
||||
]
|
||||
}
|
||||
], noopToResource),
|
||||
'*@example* \n```\n1 + 1 {@link foo}\n```');
|
||||
});
|
||||
|
||||
test('Should render @linkcode symbol name as code', () => {
|
||||
assert.strictEqual(
|
||||
asPlainTextWithLinks([
|
||||
{ "text": "a ", "kind": "text" },
|
||||
@@ -155,7 +183,7 @@ suite('typescript.previewer', () => {
|
||||
'a [`dog`](command:_typescript.openJsDocLink?%5B%7B%22file%22%3A%7B%22path%22%3A%22%2Fpath%2Ffile.ts%22%2C%22scheme%22%3A%22file%22%7D%2C%22position%22%3A%7B%22line%22%3A6%2C%22character%22%3A4%7D%7D%5D) b');
|
||||
});
|
||||
|
||||
test('Should render @linkcode text as code', async () => {
|
||||
test('Should render @linkcode text as code', () => {
|
||||
assert.strictEqual(
|
||||
asPlainTextWithLinks([
|
||||
{ "text": "a ", "kind": "text" },
|
||||
|
||||
@@ -146,7 +146,7 @@ class BufferSynchronizer {
|
||||
const closedFiles: string[] = [];
|
||||
const openFiles: Proto.OpenRequestArgs[] = [];
|
||||
const changedFiles: Proto.FileCodeEdits[] = [];
|
||||
for (const change of this._pending.values) {
|
||||
for (const change of this._pending.values()) {
|
||||
switch (change.type) {
|
||||
case BufferOperationType.Change: changedFiles.push(change.args); break;
|
||||
case BufferOperationType.Open: openFiles.push(change.args); break;
|
||||
@@ -270,13 +270,13 @@ class SyncedBufferMap extends ResourceMap<SyncedBuffer> {
|
||||
}
|
||||
|
||||
public get allBuffers(): Iterable<SyncedBuffer> {
|
||||
return this.values;
|
||||
return this.values();
|
||||
}
|
||||
}
|
||||
|
||||
class PendingDiagnostics extends ResourceMap<number> {
|
||||
public getOrderedFileSet(): ResourceMap<void> {
|
||||
const orderedResources = Array.from(this.entries)
|
||||
const orderedResources = Array.from(this.entries())
|
||||
.sort((a, b) => a.value - b.value)
|
||||
.map(entry => entry.resource);
|
||||
|
||||
@@ -313,7 +313,7 @@ class GetErrRequest {
|
||||
}
|
||||
|
||||
const supportsSyntaxGetErr = this.client.apiVersion.gte(API.v440);
|
||||
const allFiles = coalesce(Array.from(files.entries)
|
||||
const allFiles = coalesce(Array.from(files.entries())
|
||||
.filter(entry => supportsSyntaxGetErr || client.hasCapabilityForResource(entry.resource, ClientCapability.Semantic))
|
||||
.map(entry => client.toTsFilePath(entry.resource)));
|
||||
|
||||
@@ -711,7 +711,7 @@ export default class BufferSyncSupport extends Disposable {
|
||||
if (this.pendingGetErr) {
|
||||
this.pendingGetErr.cancel();
|
||||
|
||||
for (const { resource } of this.pendingGetErr.files.entries) {
|
||||
for (const { resource } of this.pendingGetErr.files.entries()) {
|
||||
if (this.syncedBuffers.get(resource)) {
|
||||
orderedFileSet.set(resource, undefined);
|
||||
}
|
||||
@@ -721,7 +721,7 @@ export default class BufferSyncSupport extends Disposable {
|
||||
}
|
||||
|
||||
// Add all open TS buffers to the geterr request. They might be visible
|
||||
for (const buffer of this.syncedBuffers.values) {
|
||||
for (const buffer of this.syncedBuffers.values()) {
|
||||
orderedFileSet.set(buffer.resource, undefined);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,31 +6,52 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Utils } from 'vscode-uri';
|
||||
import { Schemes } from '../configuration/schemes';
|
||||
import { Logger } from '../logging/logger';
|
||||
import { disposeAll, IDisposable } from '../utils/dispose';
|
||||
import { ResourceMap } from '../utils/resourceMap';
|
||||
|
||||
type DirWatcherEntry = {
|
||||
interface DirWatcherEntry {
|
||||
readonly uri: vscode.Uri;
|
||||
readonly listeners: IDisposable[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export class FileWatcherManager {
|
||||
export class FileWatcherManager implements IDisposable {
|
||||
|
||||
private readonly _fileWatchers = new Map<number, {
|
||||
readonly uri: vscode.Uri;
|
||||
readonly watcher: vscode.FileSystemWatcher;
|
||||
readonly dirWatchers: DirWatcherEntry[];
|
||||
}>();
|
||||
|
||||
private readonly _dirWatchers = new ResourceMap<{
|
||||
readonly uri: vscode.Uri;
|
||||
readonly watcher: vscode.FileSystemWatcher;
|
||||
refCount: number;
|
||||
}>(uri => uri.toString(), { onCaseInsensitiveFileSystem: false });
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
) { }
|
||||
|
||||
dispose(): void {
|
||||
for (const entry of this._fileWatchers.values()) {
|
||||
entry.watcher.dispose();
|
||||
}
|
||||
this._fileWatchers.clear();
|
||||
|
||||
for (const entry of this._dirWatchers.values()) {
|
||||
entry.watcher.dispose();
|
||||
}
|
||||
this._dirWatchers.clear();
|
||||
}
|
||||
|
||||
create(id: number, uri: vscode.Uri, watchParentDirs: boolean, isRecursive: boolean, listeners: { create?: (uri: vscode.Uri) => void; change?: (uri: vscode.Uri) => void; delete?: (uri: vscode.Uri) => void }): void {
|
||||
this.logger.trace(`Creating file watcher for ${uri.toString()}`);
|
||||
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(uri, isRecursive ? '**' : '*'), !listeners.create, !listeners.change, !listeners.delete);
|
||||
const parentDirWatchers: DirWatcherEntry[] = [];
|
||||
this._fileWatchers.set(id, { watcher, dirWatchers: parentDirWatchers });
|
||||
this._fileWatchers.set(id, { uri, watcher, dirWatchers: parentDirWatchers });
|
||||
|
||||
if (listeners.create) { watcher.onDidCreate(listeners.create); }
|
||||
if (listeners.change) { watcher.onDidChange(listeners.change); }
|
||||
@@ -43,9 +64,10 @@ export class FileWatcherManager {
|
||||
|
||||
let parentDirWatcher = this._dirWatchers.get(dirUri);
|
||||
if (!parentDirWatcher) {
|
||||
this.logger.trace(`Creating parent dir watcher for ${dirUri.toString()}`);
|
||||
const glob = new vscode.RelativePattern(Utils.dirname(dirUri), Utils.basename(dirUri));
|
||||
const parentWatcher = vscode.workspace.createFileSystemWatcher(glob, !listeners.create, true, !listeners.delete);
|
||||
parentDirWatcher = { refCount: 0, watcher: parentWatcher };
|
||||
parentDirWatcher = { uri: dirUri, refCount: 0, watcher: parentWatcher };
|
||||
this._dirWatchers.set(dirUri, parentDirWatcher);
|
||||
}
|
||||
parentDirWatcher.refCount++;
|
||||
@@ -75,15 +97,19 @@ export class FileWatcherManager {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
delete(id: number): void {
|
||||
const entry = this._fileWatchers.get(id);
|
||||
if (entry) {
|
||||
this.logger.trace(`Deleting file watcher for ${entry.uri}`);
|
||||
|
||||
for (const dirWatcher of entry.dirWatchers) {
|
||||
disposeAll(dirWatcher.listeners);
|
||||
|
||||
const dirWatcherEntry = this._dirWatchers.get(dirWatcher.uri);
|
||||
if (dirWatcherEntry) {
|
||||
if (--dirWatcherEntry.refCount <= 0) {
|
||||
this.logger.trace(`Deleting parent dir ${dirWatcherEntry.uri}`);
|
||||
dirWatcherEntry.watcher.dispose();
|
||||
this._dirWatchers.delete(dirWatcher.uri);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ServiceConnection } from '@vscode/sync-api-common/browser';
|
||||
import { ApiService, Requests } from '@vscode/sync-api-service';
|
||||
import * as vscode from 'vscode';
|
||||
import { TypeScriptServiceConfiguration } from '../configuration/configuration';
|
||||
import { Logger } from '../logging/logger';
|
||||
import { FileWatcherManager } from './fileWatchingManager';
|
||||
import type * as Proto from './protocol/protocol';
|
||||
import { TsServerLog, TsServerProcess, TsServerProcessFactory, TsServerProcessKind } from './server';
|
||||
@@ -30,6 +31,7 @@ type BrowserWatchEvent = {
|
||||
export class WorkerServerProcessFactory implements TsServerProcessFactory {
|
||||
constructor(
|
||||
private readonly _extensionUri: vscode.Uri,
|
||||
private readonly _logger: Logger,
|
||||
) { }
|
||||
|
||||
public fork(
|
||||
@@ -47,7 +49,7 @@ export class WorkerServerProcessFactory implements TsServerProcessFactory {
|
||||
// Explicitly give TS Server its path so it can
|
||||
// load local resources
|
||||
'--executingFilePath', tsServerPath,
|
||||
], tsServerLog);
|
||||
], tsServerLog, this._logger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,16 +62,16 @@ class WorkerServerProcess implements TsServerProcess {
|
||||
private readonly _onDataHandlers = new Set<(data: Proto.Response) => void>();
|
||||
private readonly _onErrorHandlers = new Set<(err: Error) => void>();
|
||||
private readonly _onExitHandlers = new Set<(code: number | null, signal: string | null) => void>();
|
||||
private readonly watches = new FileWatcherManager();
|
||||
|
||||
private readonly worker: Worker;
|
||||
private readonly _worker: Worker;
|
||||
private readonly _watches: FileWatcherManager;
|
||||
|
||||
/** For communicating with TS server synchronously */
|
||||
private readonly tsserver: MessagePort;
|
||||
private readonly _tsserver: MessagePort;
|
||||
/** For communicating watches asynchronously */
|
||||
private readonly watcher: MessagePort;
|
||||
private readonly _watcher: MessagePort;
|
||||
/** For communicating with filesystem synchronously */
|
||||
private readonly syncFs: MessagePort;
|
||||
private readonly _syncFs: MessagePort;
|
||||
|
||||
public constructor(
|
||||
private readonly kind: TsServerProcessKind,
|
||||
@@ -77,17 +79,20 @@ class WorkerServerProcess implements TsServerProcess {
|
||||
extensionUri: vscode.Uri,
|
||||
args: readonly string[],
|
||||
private readonly tsServerLog: TsServerLog | undefined,
|
||||
logger: Logger,
|
||||
) {
|
||||
this.worker = new Worker(tsServerPath, { name: `TS ${kind} server #${this.id}` });
|
||||
this._worker = new Worker(tsServerPath, { name: `TS ${kind} server #${this.id}` });
|
||||
|
||||
this._watches = new FileWatcherManager(logger);
|
||||
|
||||
const tsserverChannel = new MessageChannel();
|
||||
const watcherChannel = new MessageChannel();
|
||||
const syncChannel = new MessageChannel();
|
||||
this.tsserver = tsserverChannel.port2;
|
||||
this.watcher = watcherChannel.port2;
|
||||
this.syncFs = syncChannel.port2;
|
||||
this._tsserver = tsserverChannel.port2;
|
||||
this._watcher = watcherChannel.port2;
|
||||
this._syncFs = syncChannel.port2;
|
||||
|
||||
this.tsserver.onmessage = (event) => {
|
||||
this._tsserver.onmessage = (event) => {
|
||||
if (event.data.type === 'log') {
|
||||
console.error(`unexpected log message on tsserver channel: ${JSON.stringify(event)}`);
|
||||
return;
|
||||
@@ -97,18 +102,18 @@ class WorkerServerProcess implements TsServerProcess {
|
||||
}
|
||||
};
|
||||
|
||||
this.watcher.onmessage = (event: MessageEvent<BrowserWatchEvent>) => {
|
||||
this._watcher.onmessage = (event: MessageEvent<BrowserWatchEvent>) => {
|
||||
switch (event.data.type) {
|
||||
case 'dispose': {
|
||||
this.watches.delete(event.data.id);
|
||||
this._watches.delete(event.data.id);
|
||||
break;
|
||||
}
|
||||
case 'watchDirectory':
|
||||
case 'watchFile': {
|
||||
this.watches.create(event.data.id, vscode.Uri.from(event.data.uri), /*watchParentDirs*/ true, !!event.data.recursive, {
|
||||
change: uri => this.watcher.postMessage({ type: 'watch', event: 'change', uri }),
|
||||
create: uri => this.watcher.postMessage({ type: 'watch', event: 'create', uri }),
|
||||
delete: uri => this.watcher.postMessage({ type: 'watch', event: 'delete', uri }),
|
||||
this._watches.create(event.data.id, vscode.Uri.from(event.data.uri), /*watchParentDirs*/ true, !!event.data.recursive, {
|
||||
change: uri => this._watcher.postMessage({ type: 'watch', event: 'change', uri }),
|
||||
create: uri => this._watcher.postMessage({ type: 'watch', event: 'create', uri }),
|
||||
delete: uri => this._watcher.postMessage({ type: 'watch', event: 'delete', uri }),
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -117,7 +122,7 @@ class WorkerServerProcess implements TsServerProcess {
|
||||
}
|
||||
};
|
||||
|
||||
this.worker.onmessage = (msg: any) => {
|
||||
this._worker.onmessage = (msg: any) => {
|
||||
// for logging only
|
||||
if (msg.data.type === 'log') {
|
||||
this.appendLog(msg.data.body);
|
||||
@@ -126,7 +131,7 @@ class WorkerServerProcess implements TsServerProcess {
|
||||
console.error(`unexpected message on main channel: ${JSON.stringify(msg)}`);
|
||||
};
|
||||
|
||||
this.worker.onerror = (err: ErrorEvent) => {
|
||||
this._worker.onerror = (err: ErrorEvent) => {
|
||||
console.error('error! ' + JSON.stringify(err));
|
||||
for (const handler of this._onErrorHandlers) {
|
||||
// TODO: The ErrorEvent type might be wrong; previously this was typed as Error and didn't have the property access.
|
||||
@@ -134,7 +139,7 @@ class WorkerServerProcess implements TsServerProcess {
|
||||
}
|
||||
};
|
||||
|
||||
this.worker.postMessage(
|
||||
this._worker.postMessage(
|
||||
{ args, extensionUri },
|
||||
[syncChannel.port1, tsserverChannel.port1, watcherChannel.port1]
|
||||
);
|
||||
@@ -145,7 +150,7 @@ class WorkerServerProcess implements TsServerProcess {
|
||||
}
|
||||
|
||||
write(serverRequest: Proto.Request): void {
|
||||
this.tsserver.postMessage(serverRequest);
|
||||
this._tsserver.postMessage(serverRequest);
|
||||
}
|
||||
|
||||
onData(handler: (response: Proto.Response) => void): void {
|
||||
@@ -162,10 +167,11 @@ class WorkerServerProcess implements TsServerProcess {
|
||||
}
|
||||
|
||||
kill(): void {
|
||||
this.worker.terminate();
|
||||
this.tsserver.close();
|
||||
this.watcher.close();
|
||||
this.syncFs.close();
|
||||
this._worker.terminate();
|
||||
this._tsserver.close();
|
||||
this._watcher.close();
|
||||
this._syncFs.close();
|
||||
this._watches.dispose();
|
||||
}
|
||||
|
||||
private appendLog(msg: string) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { PluginManager, TypeScriptServerPlugin } from './tsServer/plugins';
|
||||
import { TelemetryProperties, TelemetryReporter, VSCodeTelemetryReporter } from './logging/telemetry';
|
||||
import Tracer from './logging/tracer';
|
||||
import { ProjectType, inferredProjectCompilerOptions } from './tsconfig';
|
||||
import { Schemes } from './configuration/schemes';
|
||||
|
||||
|
||||
export interface TsDiagnostics {
|
||||
@@ -762,6 +763,18 @@ export default class TypeScriptServiceClient extends Disposable implements IType
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// For notebook cells, we need to use the notebook document to look up the workspace
|
||||
if (resource.scheme === Schemes.notebookCell) {
|
||||
for (const notebook of vscode.workspace.notebookDocuments) {
|
||||
for (const cell of notebook.getCells()) {
|
||||
if (cell.document.uri.toString() === resource.toString()) {
|
||||
resource = notebook.uri;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of roots.sort((a, b) => a.uri.fsPath.length - b.uri.fsPath.length)) {
|
||||
if (root.uri.scheme === resource.scheme && root.uri.authority === resource.authority) {
|
||||
if (resource.fsPath.startsWith(root.uri.fsPath + path.sep)) {
|
||||
@@ -770,7 +783,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return vscode.workspace.getWorkspaceFolder(resource)?.uri;
|
||||
}
|
||||
|
||||
public execute(command: keyof TypeScriptRequests, args: any, token: vscode.CancellationToken, config?: ExecConfig): Promise<ServerResponse.Response<Proto.Response>> {
|
||||
|
||||
@@ -73,11 +73,11 @@ export class ResourceMap<T> {
|
||||
this._map.clear();
|
||||
}
|
||||
|
||||
public get values(): Iterable<T> {
|
||||
public values(): Iterable<T> {
|
||||
return Array.from(this._map.values(), x => x.value);
|
||||
}
|
||||
|
||||
public get entries(): Iterable<{ resource: vscode.Uri; value: T }> {
|
||||
public entries(): Iterable<{ resource: vscode.Uri; value: T }> {
|
||||
return this._map.values();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user