Skip to content

Commit 93cf8d1

Browse files
authored
Implement "Request Forwarding to tsserver" (#207)
* export types from ts-plugin * implement "Request Forwarding to tsserver" * add changelog * update limitation description in README
1 parent 45427a2 commit 93cf8d1

11 files changed

Lines changed: 287 additions & 4 deletions

File tree

.changeset/bright-hounds-cheat.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@css-modules-kit/ts-plugin': patch
3+
'css-modules-kit-vscode': patch
4+
---
5+
6+
fix: fix the issue that renaming classes from .css does not work in VS Code

.changeset/thirty-masks-push.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@css-modules-kit/ts-plugin': patch
3+
'css-modules-kit-vscode': patch
4+
---
5+
6+
fix: fix the issue that Go to Definition for specifiers fails using import alias in VS Code

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,6 @@ When this option is `true`, `import { button } from '...'` will be added. When t
199199
- Sass/Less are not supported to simplify the implementation
200200
- `:local .foo {...}` (without any arguments) is not supported to simplify the implementation
201201
- `:global .foo {...}` (without any arguments) is not supported to simplify the implementation
202-
- Some editors do not allow rename from `*.module.css`
203-
- See [#121](https://github.com/mizdra/css-modules-kit/issues/121) for more details.
204202
- css-modules-kit does not work on VS Code for Web
205203
- This is to simplify implementation.
206204

packages/ts-plugin/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@
2323
"access": "public",
2424
"registry": "https://registry.npmjs.org/"
2525
},
26+
"exports": {
27+
".": {
28+
"types": "./dist/index.d.ts",
29+
"default": "./dist/index.js"
30+
},
31+
"./type": {
32+
"types": "./dist/type.d.ts",
33+
"default": "./dist/type.js"
34+
}
35+
},
2636
"keywords": [
2737
"css-modules",
2838
"typescript",

packages/ts-plugin/src/index.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import type { CMKConfig } from '@css-modules-kit/core';
22
import { createMatchesPattern, createResolver, readConfigFile } from '@css-modules-kit/core';
33
import { TsConfigFileNotFoundError } from '@css-modules-kit/core';
4+
import type { Language } from '@volar/language-core';
45
import { createLanguageServicePlugin } from '@volar/typescript/lib/quickstart/createLanguageServicePlugin.js';
6+
import type ts from 'typescript';
57
import { createCSSLanguagePlugin } from './language-plugin.js';
68
import { proxyLanguageService } from './language-service/proxy.js';
9+
import { createDocumentLinkHandler } from './protocol-handler/documentLink.js';
10+
import { createRenameHandler } from './protocol-handler/rename.js';
11+
import { createRenameInfoHandler } from './protocol-handler/renameInfo.js';
12+
13+
const projectToLanguage = new WeakMap<ts.server.Project, Language<string>>();
714

815
const plugin = createLanguageServicePlugin((ts, info) => {
916
if (info.project.projectKind !== ts.server.ProjectKind.Configured) {
@@ -53,6 +60,7 @@ const plugin = createLanguageServicePlugin((ts, info) => {
5360
return {
5461
languagePlugins: [createCSSLanguagePlugin(resolver, matchesPattern, config)],
5562
setup: (language) => {
63+
projectToLanguage.set(info.project, language);
5664
info.languageService = proxyLanguageService(
5765
language,
5866
info.languageService,
@@ -61,6 +69,42 @@ const plugin = createLanguageServicePlugin((ts, info) => {
6169
matchesPattern,
6270
config,
6371
);
72+
if (info.session) {
73+
// Register protocol handlers for "Request Forwarding to tsserver".
74+
// See https://github.com/mizdra/css-modules-kit/pull/207 for more details.
75+
76+
// `info.session.addProtocolHandler` cannot register multiple handlers with the same command name.
77+
// Attempting to do so will result in an error.
78+
//
79+
// By the way, tsserver creates one ConfiguredProject for each tsconfig.json file. Then, tsserver
80+
// initializes each plugin for each ConfiguredProject. This means that if there are multiple
81+
// tsconfig.json files, the handler will be registered multiple times.
82+
//
83+
// Therefore, we will do the following:
84+
// - Implement the handler to handle files from different projects
85+
// - Skip registration if the handler is already registered
86+
try {
87+
info.session.addProtocolHandler('_css-modules-kit:rename', createRenameHandler(info.project.projectService));
88+
info.session.addProtocolHandler(
89+
'_css-modules-kit:renameInfo',
90+
createRenameInfoHandler(info.project.projectService),
91+
);
92+
info.session.addProtocolHandler(
93+
'_css-modules-kit:documentLink',
94+
createDocumentLinkHandler(info.project.projectService, projectToLanguage, resolver),
95+
);
96+
} catch {
97+
info.project.projectService.logger.info(
98+
`[@css-modules-kit/ts-plugin] Skipping protocol handler registration because the handlers are already registered.`,
99+
);
100+
}
101+
} else {
102+
// When a plugin is used via tsserver from the editor, the session is always available.
103+
// However, when a plugin is used via the TypeScript Compiler API, the session may not be available.
104+
info.project.projectService.logger.info(
105+
'[@css-modules-kit/ts-plugin] info: Skipping protocol handler registration because session is not available.',
106+
);
107+
}
64108
},
65109
};
66110
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { Resolver } from '@css-modules-kit/core';
2+
import type { Language } from '@volar/language-core';
3+
import type ts from 'typescript';
4+
import { CMK_DATA_KEY, isCSSModuleScript } from '../language-plugin.js';
5+
import type {
6+
CSSModulesKitDocumentLinkHandlerResponse,
7+
CSSModulesKitDocumentLinkRequest,
8+
DocumentLink,
9+
} from '../type.js';
10+
import { getConfiguredProjectForFile } from '../util.js';
11+
12+
export function createDocumentLinkHandler(
13+
projectService: ts.server.ProjectService,
14+
projectToLanguage: WeakMap<ts.server.Project, Language<string>>,
15+
resolver: Resolver,
16+
) {
17+
return (request: CSSModulesKitDocumentLinkRequest): CSSModulesKitDocumentLinkHandlerResponse => {
18+
const { fileName } = request.arguments;
19+
const project = getConfiguredProjectForFile(projectService, fileName);
20+
if (!project) return {};
21+
const language = projectToLanguage.get(project);
22+
if (!language) return {};
23+
const script = language.scripts.get(fileName);
24+
const links: DocumentLink[] = [];
25+
if (isCSSModuleScript(script)) {
26+
const { tokenImporters } = script.generated.root[CMK_DATA_KEY].cssModule;
27+
for (const { from, fromLoc } of tokenImporters) {
28+
const resolved = resolver(from, { request: fileName });
29+
if (!resolved) continue;
30+
links.push({
31+
fileName: resolved,
32+
textSpan: { start: fromLoc.start.offset, length: fromLoc.end.offset - fromLoc.start.offset },
33+
});
34+
}
35+
}
36+
return { response: { result: links } };
37+
};
38+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import ts from 'typescript';
2+
import type { CSSModulesKitRenameHandlerResponse, CSSModulesKitRenameRequest } from '../type.js';
3+
import { getConfiguredProjectForFile } from '../util.js';
4+
5+
export function createRenameHandler(projectService: ts.server.ProjectService) {
6+
return (request: CSSModulesKitRenameRequest): CSSModulesKitRenameHandlerResponse => {
7+
const { fileName, position } = request.arguments;
8+
const project = getConfiguredProjectForFile(projectService, fileName);
9+
if (!project) return {};
10+
const languageService = project.getLanguageService();
11+
const preference = project.projectService.getPreferences(ts.server.toNormalizedPath(fileName));
12+
const result = languageService.findRenameLocations(fileName, position, false, false, preference);
13+
return { response: { result } };
14+
};
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import ts from 'typescript';
2+
import type { CSSModulesKitRenameInfoHandlerResponse, CSSModulesKitRenameInfoRequest } from '../type.js';
3+
import { getConfiguredProjectForFile } from '../util.js';
4+
5+
export function createRenameInfoHandler(projectService: ts.server.ProjectService) {
6+
return (request: CSSModulesKitRenameInfoRequest): CSSModulesKitRenameInfoHandlerResponse => {
7+
const { fileName, position } = request.arguments;
8+
const project = getConfiguredProjectForFile(projectService, fileName);
9+
if (!project) return {};
10+
const languageService = project.getLanguageService();
11+
const preference = project.projectService.getPreferences(ts.server.toNormalizedPath(fileName));
12+
const result = languageService.getRenameInfo(fileName, position, preference);
13+
return { response: { result } };
14+
};
15+
}

packages/ts-plugin/src/type.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type ts from 'typescript';
2+
3+
export interface CSSModulesKitRenameRequest extends ts.server.protocol.Request {
4+
command: '_css-modules-kit:rename';
5+
arguments: { fileName: string; position: number };
6+
}
7+
export interface CSSModulesKitRenameHandlerResponse extends ts.server.HandlerResponse {
8+
response?: { result: ReturnType<ts.LanguageService['findRenameLocations']> };
9+
}
10+
export interface CSSModulesKitRenameResponse extends ts.server.protocol.Response {
11+
command: '_css-modules-kit:rename';
12+
readonly body: CSSModulesKitRenameHandlerResponse['response'];
13+
}
14+
15+
export interface CSSModulesKitRenameInfoRequest extends ts.server.protocol.Request {
16+
command: '_css-modules-kit:renameInfo';
17+
arguments: { fileName: string; position: number };
18+
}
19+
export interface CSSModulesKitRenameInfoHandlerResponse extends ts.server.HandlerResponse {
20+
response?: { result: ReturnType<ts.LanguageService['getRenameInfo']> };
21+
}
22+
export interface CSSModulesKitRenameInfoResponse extends ts.server.protocol.Response {
23+
command: '_css-modules-kit:renameInfo';
24+
readonly body: CSSModulesKitRenameInfoHandlerResponse['response'];
25+
}
26+
27+
export interface DocumentLink {
28+
fileName: string;
29+
textSpan: ts.TextSpan;
30+
}
31+
export interface CSSModulesKitDocumentLinkRequest extends ts.server.protocol.Request {
32+
command: '_css-modules-kit:documentLink';
33+
arguments: { fileName: string };
34+
}
35+
export interface CSSModulesKitDocumentLinkHandlerResponse extends ts.server.HandlerResponse {
36+
response?: { result: DocumentLink[] };
37+
}
38+
export interface CSSModulesKitDocumentLinkResponse extends ts.server.protocol.Response {
39+
command: '_css-modules-kit:documentLink';
40+
readonly body: CSSModulesKitDocumentLinkHandlerResponse['response'];
41+
}
42+
43+
declare module 'typescript' {
44+
// eslint-disable-next-line @typescript-eslint/no-namespace
45+
namespace server {
46+
export interface Session {
47+
addProtocolHandler(
48+
command: '_css-modules-kit:rename',
49+
handler: (request: CSSModulesKitRenameRequest) => CSSModulesKitRenameHandlerResponse,
50+
): void;
51+
addProtocolHandler(
52+
command: '_css-modules-kit:renameInfo',
53+
handler: (request: CSSModulesKitRenameInfoRequest) => CSSModulesKitRenameInfoHandlerResponse,
54+
): void;
55+
addProtocolHandler(
56+
command: '_css-modules-kit:documentLink',
57+
handler: (request: CSSModulesKitDocumentLinkRequest) => CSSModulesKitDocumentLinkHandlerResponse,
58+
): void;
59+
}
60+
}
61+
}

packages/ts-plugin/src/util.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,12 @@ export function convertDefaultImportsToNamespaceImports(
5656
}
5757
}
5858
}
59+
60+
export function getConfiguredProjectForFile(
61+
projectService: ts.server.ProjectService,
62+
fileName: string,
63+
): ts.server.ConfiguredProject | undefined {
64+
const project = projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(fileName), false);
65+
if (!project || project.projectKind !== ts.server.ProjectKind.Configured) return;
66+
return project as ts.server.ConfiguredProject;
67+
}

0 commit comments

Comments
 (0)