Skip to content
Closed
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
4 changes: 3 additions & 1 deletion packages/vscode-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"onLanguage:javascript",
"onLanguage:javascriptreact",
"onLanguage:typescript",
"onLanguage:typescriptreact"
"onLanguage:typescriptreact",
"workspaceContains:**/tsconfig.json",
"workspaceContains:**/jsconfig.json"
],
"contributes": {
"configuration": [
Expand Down
15 changes: 15 additions & 0 deletions packages/vscode-typescript/src/contentMapperContributions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import * as vscode from "vscode";

export const jsTsLanguageModes = [
"typescript",
"typescriptreact",
"javascript",
"javascriptreact",
];

export interface ContentMapperManifest {
readonly name: string;
readonly version?: string;
Expand Down Expand Up @@ -92,3 +99,11 @@ export function documentMatchesContentMapperContributions(
}
return false;
}

const candidateSchemes = new Set(["file", "untitled"]);

export function documentIsContentMapperCandidate(
document: { readonly uri: { readonly scheme: string; }; readonly languageId: string; },
): boolean {
return candidateSchemes.has(document.uri.scheme) && !jsTsLanguageModes.includes(document.languageId);
}
13 changes: 11 additions & 2 deletions packages/vscode-typescript/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "./commands";
import {
type ContentMapperContribution,
documentIsContentMapperCandidate,
documentMatchesContentMapperContributions,
serializeContentMapperContributions,
validateContentMapperRegistration,
Expand All @@ -16,6 +17,7 @@ import { ProjectStatus } from "./projectStatus";
import { setupStatusBar } from "./statusBar";
import { TelemetryReporter } from "./telemetryReporting";
import {
contentMappersEnabled,
getDefaultExePath,
getExe,
getWorkspaceTsdkConfigValue,
Expand Down Expand Up @@ -60,7 +62,7 @@ export class SessionManager implements vscode.Disposable {
}
}));
this.disposables.push(vscode.workspace.onDidOpenTextDocument(document => {
if (documentMatchesContentMapperContributions(document, this.contentMapperRegistrations)) {
if (this.shouldSyncForDocument(document)) {
void this.syncContentMapperContributions();
}
}));
Expand Down Expand Up @@ -136,11 +138,18 @@ export class SessionManager implements vscode.Disposable {
return operation;
}

private shouldSyncForDocument(document: vscode.TextDocument): boolean {
if (documentMatchesContentMapperContributions(document, this.contentMapperRegistrations)) {
return true;
}
return contentMappersEnabled() && documentIsContentMapperCandidate(document);
}

private async syncContentMapperContributionsNow(): Promise<void> {
try {
if (!this.currentSession?.client.isInitialized) return;
const openDocuments = vscode.workspace.textDocuments
.filter(document => documentMatchesContentMapperContributions(document, this.contentMapperRegistrations))
.filter(document => this.shouldSyncForDocument(document))
Comment on lines 151 to +152
.map(document => document.uri);
await this.currentSession.client.setContentMapperContributions(
serializeContentMapperContributions(this.contentMapperRegistrations),
Expand Down
10 changes: 3 additions & 7 deletions packages/vscode-typescript/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import * as fs from "fs";
import * as path from "path";
import * as vscode from "vscode";
import { jsTsLanguageModes } from "./contentMapperContributions";
import { resolvePackageExecutable } from "./tsdkPackage";

export { jsTsLanguageModes };

export const aiConnectionString = "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255";

export const languageClientName = "TypeScript Language Server";
export const nightlyExtensionId = "TypeScriptTeam.vscode-typescript-nightly";
export const enableContributedNightlyVersion = true;

export const jsTsLanguageModes = [
"typescript",
"typescriptreact",
"javascript",
"javascriptreact",
];

export const builtinTSExtensionId = "vscode.typescript-language-features";

/**
Expand Down
19 changes: 19 additions & 0 deletions packages/vscode-typescript/test/contentMapperContributions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";
import {
type ContentMapperContribution,
documentIsContentMapperCandidate,
documentMatchesContentMapperContributions,
serializeContentMapperContributions,
} from "../src/contentMapperContributions";
Expand Down Expand Up @@ -30,6 +31,24 @@ test("content mapper extensions match document paths case-insensitively", () =>
assert.equal(documentMatchesContentMapperContributions(document, registrations), true);
});

test("content mapper candidates are non-js/ts file and untitled documents", () => {
assert.equal(documentIsContentMapperCandidate({ uri: { scheme: "file" }, languageId: "plaintext" }), true);
assert.equal(documentIsContentMapperCandidate({ uri: { scheme: "file" }, languageId: "unknown" }), true);
assert.equal(documentIsContentMapperCandidate({ uri: { scheme: "untitled" }, languageId: "plaintext" }), true);
});
Comment on lines +34 to +38

test("content mapper candidates exclude js/ts documents", () => {
for (const languageId of ["typescript", "typescriptreact", "javascript", "javascriptreact"]) {
assert.equal(documentIsContentMapperCandidate({ uri: { scheme: "file" }, languageId }), false);
assert.equal(documentIsContentMapperCandidate({ uri: { scheme: "untitled" }, languageId }), false);
}
});

test("content mapper candidates exclude non-file schemes", () => {
assert.equal(documentIsContentMapperCandidate({ uri: { scheme: "git" }, languageId: "plaintext" }), false);
assert.equal(documentIsContentMapperCandidate({ uri: { scheme: "output" }, languageId: "plaintext" }), false);
});

test("serializes the documented inferred project contribution", () => {
const registrations = new Map<string, readonly ContentMapperContribution[]>([[
"publisher.extension",
Expand Down
2 changes: 1 addition & 1 deletion tsc/internal/lsp/lsproto/_generate/generate.mts
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ const customStructures: Structure[] = [
name: "SetContentMapperContributionsParams",
properties: [
{ name: "contributions", type: { kind: "array", element: { kind: "reference", name: "ContentMapperContribution" } }, documentation: "Complete replacement set of active extension contributions." },
{ name: "openDocuments", type: { kind: "array", element: { kind: "reference", name: "TextDocumentIdentifier" } }, documentation: "Currently open documents matching contributed extensions." },
{ name: "openDocuments", type: { kind: "array", element: { kind: "reference", name: "TextDocumentIdentifier" } }, documentation: "Currently open candidate documents used to discover the configured projects that declare their content mappers." },
],
documentation: "Parameters for the custom/setContentMapperContributions request.",
},
Expand Down
2 changes: 1 addition & 1 deletion tsc/internal/lsp/lsproto/lsp_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 94 additions & 0 deletions tsc/internal/lsp/server_contentmapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,97 @@ export const title = "Profile";
TextDocument: lsproto.TextDocumentIdentifier{Uri: uri},
})
}

func TestSetContentMapperCandidatesDiscoverConfiguredProject(t *testing.T) {
t.Parallel()
if !bundled.Embedded {
t.Skip("bundled files are not embedded")
}

const component = `<component name="ProfileCard">
<template><h1>{{ title }}</h1></template>
<script lang="ts">
export const title = "Profile";
</script>`
files := map[string]string{
"/home/project/tsconfig.json": `{
"compilerOptions": { "target": "es2020", "module": "esnext", "moduleResolution": "bundler", "strict": true },
"contentMappers": [ { "package": "mapper", "extensions": [".vue"] } ]
}`,
"/home/project/node_modules/mapper/package.json": contentmappertest.PackageJSON(contentmappertest.ComponentMapper),
"/home/project/ProfileCard.vue": component,
}

var mu sync.Mutex
var didOpenRegistration *lsproto.Registration
onServerRequest := func(_ context.Context, req *lsproto.RequestMessage) *lsproto.ResponseMessage {
switch req.Method {
case lsproto.MethodWorkspaceConfiguration:
return &lsproto.ResponseMessage{ID: req.ID, JSONRPC: req.JSONRPC, Result: []any{nil, nil, nil, nil}}
case lsproto.MethodClientRegisterCapability:
params, err := req.UnmarshalParams[*lsproto.RegistrationParams]()
assert.NilError(t, err)
mu.Lock()
defer mu.Unlock()
for _, registration := range params.Registrations {
if registration.Id == "content-mapper-did-open" {
didOpenRegistration = registration
}
}
return &lsproto.ResponseMessage{ID: req.ID, JSONRPC: req.JSONRPC, Result: lsproto.Null{}}
default:
return nil
}
}

fs := bundled.WrapFS(vfstest.FromMap(files, false))
client, closeClient := lsptestutil.NewLSPClient(t, lsp.ServerOptions{
Err: io.Discard,
Cwd: "/home/project",
FS: fs,
DefaultLibraryPath: bundled.LibPath(),
Spawn: contentmappertest.NewSpawner().Spawn,
}, onServerRequest)
t.Cleanup(func() { _ = closeClient() })

caps := &lsproto.ClientCapabilities{
TextDocument: &lsproto.TextDocumentClientCapabilities{
Synchronization: &lsproto.TextDocumentSyncClientCapabilities{DynamicRegistration: new(true)},
},
}
initMsg, _, ok := client.SendRequest(t, lsproto.InitializeInfo, &lsproto.InitializeParams{
Capabilities: caps,
InitializationOptions: &lsproto.InitializationOptionsOrNull{InitializationOptions: &lsproto.InitializationOptions{
RunExternalCode: new(true),
}},
})
assert.Assert(t, ok && initMsg.AsResponse().Error == nil, "initialize failed")
client.SendNotification(t, lsproto.InitializedInfo, &lsproto.InitializedParams{})
<-client.Server.InitComplete()

uri := lsproto.DocumentUri("file:///home/project/ProfileCard.vue")
msg, _, ok := client.SendRequest(t, lsproto.CustomSetContentMapperContributionsInfo, &lsproto.SetContentMapperContributionsParams{
Contributions: []*lsproto.ContentMapperContribution{},
OpenDocuments: []lsproto.TextDocumentIdentifier{{Uri: uri}},
})
assert.Assert(t, ok && msg.AsResponse().Error == nil)

mu.Lock()
registration := didOpenRegistration
mu.Unlock()
assert.Assert(t, registration != nil, "expected content-mapper-did-open registration for tsconfig-declared mapper")
assert.Assert(t, registration.RegisterOptions != nil && registration.RegisterOptions.TextDocumentDidOpen != nil)
selector := registration.RegisterOptions.TextDocumentDidOpen.DocumentSelector.DocumentSelector
assert.Assert(t, selector != nil && len(*selector) == 1)
assert.Equal(t, *(*selector)[0].Pattern.Pattern.Pattern, "**/*.vue")

client.SendNotification(t, lsproto.TextDocumentDidOpenInfo, &lsproto.DidOpenTextDocumentParams{
TextDocument: &lsproto.TextDocumentItem{Uri: uri, LanguageId: "vue", Version: 1, Text: component},
})
hoverMsg, hover, ok := client.SendRequest(t, lsproto.TextDocumentHoverInfo, &lsproto.HoverParams{
TextDocument: lsproto.TextDocumentIdentifier{Uri: uri},
Position: lsproto.Position{Line: 3, Character: 15},
})
assert.Assert(t, ok && hoverMsg.AsResponse().Error == nil)
assert.Assert(t, hover.Hover != nil, "expected hover for mapped-only file without opening a TS file")
}