Merge branch 'main' into ai-inferFromUsage

This commit is contained in:
Nathan Shively-Sanders
2023-08-14 16:41:26 -07:00
1517 changed files with 69345 additions and 26743 deletions

View File

@@ -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,
]);

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -195,6 +195,7 @@ export default class FileConfigurationManager extends Disposable {
allowIncompleteCompletions: true,
displayPartsForJSDoc: true,
disableLineTextInReferences: true,
interactiveInlayHints: true,
...getInlayHintsPreferences(config),
};

View File

@@ -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 {

View File

@@ -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) => {

View File

@@ -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;
}
}
}

View 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`,
]
]);
}

View File

@@ -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(

View File

@@ -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" },

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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>> {

View File

@@ -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();
}