Skip to content

Commit 1481fd2

Browse files
committed
Add basic mulit-root workspaces support
1 parent 2ea0666 commit 1481fd2

File tree

4 files changed

+109
-91
lines changed

4 files changed

+109
-91
lines changed

server/src/services/dependencyService.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ import { getPathDepth } from '../utils/paths';
1717
const readFileAsync = util.promisify(fs.readFile);
1818
const accessFileAsync = util.promisify(fs.access);
1919

20-
async function createNodeModulesPaths(rootPath: string) {
20+
export function createNodeModulesPaths(rootPath: string) {
2121
const startTime = performance.now();
22-
const nodeModules = await fg('**/node_modules', {
22+
const nodeModules = fg.sync('**/node_modules', {
2323
cwd: rootPath.replace(/\\/g, '/'),
2424
absolute: true,
2525
unique: true,
@@ -93,6 +93,7 @@ export interface DependencyService {
9393
rootPathForConfig: string,
9494
workspacePath: string,
9595
useWorkspaceDependencies: boolean,
96+
nodeModulesPaths: string[],
9697
tsSDKPath?: string
9798
): Promise<void>;
9899
get<L extends keyof RuntimeLibrary>(lib: L, filePath?: string): Dependency<RuntimeLibrary[L]>;
@@ -108,10 +109,9 @@ export const createDependencyService = (): DependencyService => {
108109
rootPathForConfig: string,
109110
workspacePath: string,
110111
useWorkspaceDependencies: boolean,
112+
nodeModulesPaths: string[],
111113
tsSDKPath?: string
112114
) {
113-
const nodeModulesPaths = useWorkspaceDependencies ? await createNodeModulesPaths(rootPathForConfig) : [];
114-
115115
const loadTypeScript = async (): Promise<Dependency<typeof ts>[]> => {
116116
try {
117117
if (useWorkspaceDependencies && tsSDKPath) {

server/src/services/projectService.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { DocumentService } from './documentService';
4242
import { VueInfoService } from './vueInfoService';
4343

4444
export interface ProjectService {
45+
readonly rootPathForConfig: string;
4546
languageModes: LanguageModes;
4647
configure(config: VLSFullConfig): void;
4748
onDocumentFormatting(params: DocumentFormattingParams): Promise<TextEdit[]>;
@@ -65,7 +66,6 @@ export interface ProjectService {
6566

6667
export async function createProjectService(
6768
rootPathForConfig: string,
68-
workspacePath: string,
6969
projectPath: string,
7070
tsconfigPath: string | undefined,
7171
packagePath: string | undefined,
@@ -119,9 +119,14 @@ export async function createProjectService(
119119
configure(initialConfig);
120120

121121
return {
122+
rootPathForConfig,
122123
configure,
123124
languageModes,
124125
async onDocumentFormatting({ textDocument, options }) {
126+
if (!$config.vetur.format.enable) {
127+
return [];
128+
}
129+
125130
const doc = documentService.getDocument(textDocument.uri)!;
126131

127132
const modeRanges = languageModes.getAllLanguageModeRangesInDocument(doc);

server/src/services/vls.ts

+98-80
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ import {
2626
CompletionParams,
2727
ExecuteCommandParams,
2828
ApplyWorkspaceEditRequest,
29-
FoldingRangeParams
29+
FoldingRangeParams,
30+
DidChangeWorkspaceFoldersNotification
3031
} from 'vscode-languageserver';
3132
import {
3233
ColorInformation,
3334
CompletionItem,
3435
CompletionList,
3536
Definition,
36-
Diagnostic,
3737
DocumentHighlight,
3838
DocumentLink,
3939
Hover,
@@ -48,85 +48,68 @@ import {
4848
import type { TextDocument } from 'vscode-languageserver-textdocument';
4949

5050
import { URI } from 'vscode-uri';
51-
import { LanguageModes, LanguageModeRange, LanguageMode } from '../embeddedSupport/languageModes';
5251
import { NULL_COMPLETION, NULL_HOVER, NULL_SIGNATURE } from '../modes/nullMode';
53-
import { VueInfoService } from './vueInfoService';
54-
import { createDependencyService, DependencyService } from './dependencyService';
52+
import { createDependencyService, createNodeModulesPaths } from './dependencyService';
5553
import _ from 'lodash';
56-
import { DocumentContext, RefactorAction } from '../types';
54+
import { RefactorAction } from '../types';
5755
import { DocumentService } from './documentService';
5856
import { VueHTMLMode } from '../modes/template';
5957
import { logger } from '../log';
60-
import { getDefaultVLSConfig, VLSFullConfig, VLSConfig, getVeturFullConfig, VeturFullConfig } from '../config';
61-
import { LanguageId } from '../embeddedSupport/embeddedSupport';
58+
import { getDefaultVLSConfig, VLSFullConfig, getVeturFullConfig, VeturFullConfig } from '../config';
6259
import { APPLY_REFACTOR_COMMAND } from '../modes/script/javascript';
6360
import { VCancellationToken, VCancellationTokenSource } from '../utils/cancellationToken';
6461
import { findConfigFile } from '../utils/workspace';
6562
import { createProjectService, ProjectService } from './projectService';
6663

6764
export class VLS {
6865
// @Todo: Remove this and DocumentContext
69-
private workspacePath: string | undefined;
70-
private veturConfig: VeturFullConfig;
66+
// private workspacePath: string | undefined;
67+
private workspaces: Map<string, VeturFullConfig & { workspaceFsPath: string }>;
68+
private nodeModulesMap: Map<string, string[]>;
69+
// private veturConfig: VeturFullConfig;
7170
private documentService: DocumentService;
72-
private rootPathForConfig: string;
71+
// private rootPathForConfig: string;
7372
private globalSnippetDir: string;
7473
private projects: Map<string, ProjectService>;
75-
private dependencyService: DependencyService;
74+
// private dependencyService: DependencyService;
7675

7776
private pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {};
7877
private cancellationTokenValidationRequests: { [uri: string]: VCancellationTokenSource } = {};
7978
private validationDelayMs = 200;
80-
private validation: { [k: string]: boolean } = {
81-
'vue-html': true,
82-
html: true,
83-
css: true,
84-
scss: true,
85-
less: true,
86-
postcss: true,
87-
javascript: true
88-
};
89-
private templateInterpolationValidation = false;
9079

9180
private documentFormatterRegistration: Disposable | undefined;
9281

93-
private config: VLSFullConfig;
82+
private workspaceConfig: VLSFullConfig;
9483

9584
constructor(private lspConnection: Connection) {
9685
this.documentService = new DocumentService(this.lspConnection);
97-
this.dependencyService = createDependencyService();
86+
this.projects = new Map();
87+
this.nodeModulesMap = new Map();
9888
}
9989

10090
async init(params: InitializeParams) {
101-
const workspacePath = params.rootPath;
102-
if (!workspacePath) {
91+
let workspaceFolders =
92+
!Array.isArray(params.workspaceFolders) && params.rootPath
93+
? [{ name: '', fsPath: normalizeFileNameToFsPath(params.rootPath) }]
94+
: params.workspaceFolders?.map(el => ({ name: el.name, fsPath: getFileFsPath(el.uri) })) ?? [];
95+
96+
if (workspaceFolders.length === 0) {
10397
console.error('No workspace path found. Vetur initialization failed.');
10498
return {
10599
capabilities: {}
106100
};
107101
}
108102

109-
this.workspacePath = normalizeFileNameToFsPath(workspacePath);
110-
103+
this.workspaces = new Map();
111104
this.globalSnippetDir = params.initializationOptions?.globalSnippetDir;
112-
const veturConfigPath = findConfigFile(this.workspacePath, 'vetur.config.js');
113-
this.rootPathForConfig = normalizeFileNameToFsPath(veturConfigPath ? path.dirname(veturConfigPath) : workspacePath);
114-
this.veturConfig = await getVeturFullConfig(
115-
this.rootPathForConfig,
116-
this.workspacePath,
117-
veturConfigPath ? require(veturConfigPath) : {}
118-
);
119-
const config = this.getFullConfig(params.initializationOptions?.config);
120105

121-
await this.dependencyService.init(
122-
this.rootPathForConfig,
123-
this.workspacePath,
124-
config.vetur.useWorkspaceDependencies,
125-
config.typescript.tsdk
126-
);
127-
this.projects = new Map();
106+
await Promise.all(workspaceFolders.map(workspace => this.addWorkspace(workspace)));
107+
108+
this.workspaceConfig = this.getVLSFullConfig({}, params.initializationOptions?.config);
128109

129-
this.configure(config);
110+
if (params.capabilities.workspace?.workspaceFolders) {
111+
this.setupWorkspaceListeners();
112+
}
130113
this.setupConfigListeners();
131114
this.setupLSPHandlers();
132115
this.setupCustomLSPHandlers();
@@ -141,28 +124,70 @@ export class VLS {
141124
this.lspConnection.listen();
142125
}
143126

144-
private getFullConfig(config: any | undefined): VLSFullConfig {
127+
private getVLSFullConfig(settings: VeturFullConfig['settings'], config: any | undefined): VLSFullConfig {
145128
const result = config ? _.merge(getDefaultVLSConfig(), config) : getDefaultVLSConfig();
146-
Object.keys(this.veturConfig.settings).forEach(key => {
147-
_.set(result, key, this.veturConfig.settings[key]);
129+
Object.keys(settings).forEach(key => {
130+
_.set(result, key, settings[key]);
148131
});
149132
return result;
150133
}
151134

135+
private async addWorkspace(workspace: { name: string; fsPath: string }) {
136+
const veturConfigPath = findConfigFile(workspace.fsPath, 'vetur.config.js');
137+
const rootPathForConfig = normalizeFileNameToFsPath(
138+
veturConfigPath ? path.dirname(veturConfigPath) : workspace.fsPath
139+
);
140+
if (!this.workspaces.has(rootPathForConfig)) {
141+
this.workspaces.set(rootPathForConfig, {
142+
...(await getVeturFullConfig(
143+
rootPathForConfig,
144+
workspace.fsPath,
145+
veturConfigPath ? require(veturConfigPath) : {}
146+
)),
147+
workspaceFsPath: workspace.fsPath
148+
});
149+
}
150+
}
151+
152+
private setupWorkspaceListeners() {
153+
this.lspConnection.client.register(DidChangeWorkspaceFoldersNotification.type);
154+
this.lspConnection.workspace.onDidChangeWorkspaceFolders(async e => {
155+
await Promise.all(e.added.map(el => this.addWorkspace({ name: el.name, fsPath: getFileFsPath(el.uri) })));
156+
});
157+
}
158+
152159
private setupConfigListeners() {
153160
this.lspConnection.onDidChangeConfiguration(async ({ settings }: DidChangeConfigurationParams) => {
154-
const config = this.getFullConfig(settings);
155-
this.configure(config);
156-
this.setupDynamicFormatters(config);
161+
let isFormatEnable = false;
162+
this.projects.forEach(project => {
163+
const veturConfig = this.workspaces.get(project.rootPathForConfig);
164+
if (!veturConfig) return;
165+
const fullConfig = this.getVLSFullConfig(veturConfig.settings, settings);
166+
project.configure(fullConfig);
167+
isFormatEnable = isFormatEnable || fullConfig.vetur.format.enable;
168+
});
169+
this.setupDynamicFormatters(isFormatEnable);
157170
});
158171

159172
this.documentService.getAllDocuments().forEach(this.triggerValidation);
160173
}
161174

162175
private async getProjectService(uri: DocumentUri): Promise<ProjectService | undefined> {
163-
const projectRootPaths = this.veturConfig.projects
176+
const projectRootPaths = _.flatten(
177+
Array.from(this.workspaces.entries()).map(([rootPathForConfig, veturConfig]) =>
178+
veturConfig.projects.map(project => ({
179+
...project,
180+
rootPathForConfig,
181+
vlsFullConfig: this.getVLSFullConfig(veturConfig.settings, this.workspaceConfig),
182+
workspaceFsPath: veturConfig.workspaceFsPath
183+
}))
184+
)
185+
)
164186
.map(project => ({
165-
rootFsPath: normalizeFileNameResolve(this.rootPathForConfig, project.root),
187+
vlsFullConfig: project.vlsFullConfig,
188+
rootPathForConfig: project.rootPathForConfig,
189+
workspaceFsPath: project.workspaceFsPath,
190+
rootFsPath: normalizeFileNameResolve(project.rootPathForConfig, project.root),
166191
tsconfigPath: project.tsconfig,
167192
packagePath: project.package,
168193
snippetFolder: project.snippetFolder,
@@ -178,18 +203,31 @@ export class VLS {
178203
return this.projects.get(projectConfig.rootFsPath);
179204
}
180205

206+
const dependencyService = createDependencyService();
207+
const nodeModulePaths =
208+
this.nodeModulesMap.get(projectConfig.rootPathForConfig) ??
209+
createNodeModulesPaths(projectConfig.rootPathForConfig);
210+
if (this.nodeModulesMap.has(projectConfig.rootPathForConfig)) {
211+
this.nodeModulesMap.set(projectConfig.rootPathForConfig, nodeModulePaths);
212+
}
213+
await dependencyService.init(
214+
projectConfig.rootPathForConfig,
215+
projectConfig.workspaceFsPath,
216+
projectConfig.vlsFullConfig.vetur.useWorkspaceDependencies,
217+
nodeModulePaths,
218+
projectConfig.vlsFullConfig.typescript.tsdk
219+
);
181220
const project = await createProjectService(
182-
this.rootPathForConfig,
183-
this.workspacePath ?? this.rootPathForConfig,
221+
projectConfig.rootPathForConfig,
184222
projectConfig.rootFsPath,
185223
projectConfig.tsconfigPath,
186224
projectConfig.packagePath,
187225
projectConfig.snippetFolder,
188226
projectConfig.globalComponents,
189227
this.documentService,
190-
this.config,
228+
this.workspaceConfig,
191229
this.globalSnippetDir,
192-
this.dependencyService
230+
dependencyService
193231
);
194232
this.projects.set(projectConfig.rootFsPath, project);
195233
return project;
@@ -232,8 +270,8 @@ export class VLS {
232270
});
233271
}
234272

235-
private async setupDynamicFormatters(settings: VLSFullConfig) {
236-
if (settings.vetur.format.enable) {
273+
private async setupDynamicFormatters(enable: boolean) {
274+
if (enable) {
237275
if (!this.documentFormatterRegistration) {
238276
this.documentFormatterRegistration = await this.lspConnection.client.register(DocumentFormattingRequest.type, {
239277
documentSelector: [{ language: 'vue' }]
@@ -270,26 +308,6 @@ export class VLS {
270308
});
271309
}
272310

273-
configure(config: VLSConfig): void {
274-
this.config = config;
275-
276-
const veturValidationOptions = config.vetur.validation;
277-
this.validation['vue-html'] = veturValidationOptions.template;
278-
this.validation.css = veturValidationOptions.style;
279-
this.validation.postcss = veturValidationOptions.style;
280-
this.validation.scss = veturValidationOptions.style;
281-
this.validation.less = veturValidationOptions.style;
282-
this.validation.javascript = veturValidationOptions.script;
283-
284-
this.templateInterpolationValidation = config.vetur.experimental.templateInterpolationService;
285-
286-
this.projects.forEach(project => {
287-
project.configure(config);
288-
});
289-
290-
logger.setLevel(config.vetur.dev.logLevel);
291-
}
292-
293311
/**
294312
* Custom Notifications
295313
*/
@@ -407,8 +425,7 @@ export class VLS {
407425
this.cancelPastValidation(textDocument);
408426
this.pendingValidationRequests[textDocument.uri] = setTimeout(() => {
409427
delete this.pendingValidationRequests[textDocument.uri];
410-
const tsDep = this.dependencyService.get('typescript');
411-
this.cancellationTokenValidationRequests[textDocument.uri] = new VCancellationTokenSource(tsDep.module);
428+
this.cancellationTokenValidationRequests[textDocument.uri] = new VCancellationTokenSource();
412429
this.validateTextDocument(textDocument, this.cancellationTokenValidationRequests[textDocument.uri].token);
413430
}, this.validationDelayMs);
414431
}
@@ -470,6 +487,7 @@ export class VLS {
470487
get capabilities(): ServerCapabilities {
471488
return {
472489
textDocumentSync: TextDocumentSyncKind.Incremental,
490+
workspace: { workspaceFolders: { supported: true, changeNotifications: true } },
473491
completionProvider: { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', "'", '/', '@', '*', ' '] },
474492
signatureHelpProvider: { triggerCharacters: ['('] },
475493
documentFormattingProvider: false,

server/src/utils/cancellationToken.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,15 @@ export interface VCancellationToken extends LSPCancellationToken {
77
}
88

99
export class VCancellationTokenSource extends CancellationTokenSource {
10-
constructor(private tsModule: RuntimeLibrary['typescript']) {
11-
super();
12-
}
13-
1410
get token(): VCancellationToken {
15-
const operationCancelException = this.tsModule.OperationCanceledException;
1611
const token = super.token as VCancellationToken;
1712
token.tsToken = {
1813
isCancellationRequested() {
1914
return token.isCancellationRequested;
2015
},
2116
throwIfCancellationRequested() {
2217
if (token.isCancellationRequested) {
23-
throw new operationCancelException();
18+
throw new Error('OperationCanceledException');
2419
}
2520
}
2621
};

0 commit comments

Comments
 (0)