Skip to content

Commit

Permalink
Add basic mulit-root workspaces support
Browse files Browse the repository at this point in the history
  • Loading branch information
yoyo930021 committed Nov 29, 2020
1 parent 2ea0666 commit 1481fd2
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 91 deletions.
8 changes: 4 additions & 4 deletions server/src/services/dependencyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import { getPathDepth } from '../utils/paths';
const readFileAsync = util.promisify(fs.readFile);
const accessFileAsync = util.promisify(fs.access);

async function createNodeModulesPaths(rootPath: string) {
export function createNodeModulesPaths(rootPath: string) {
const startTime = performance.now();
const nodeModules = await fg('**/node_modules', {
const nodeModules = fg.sync('**/node_modules', {
cwd: rootPath.replace(/\\/g, '/'),
absolute: true,
unique: true,
Expand Down Expand Up @@ -93,6 +93,7 @@ export interface DependencyService {
rootPathForConfig: string,
workspacePath: string,
useWorkspaceDependencies: boolean,
nodeModulesPaths: string[],
tsSDKPath?: string
): Promise<void>;
get<L extends keyof RuntimeLibrary>(lib: L, filePath?: string): Dependency<RuntimeLibrary[L]>;
Expand All @@ -108,10 +109,9 @@ export const createDependencyService = (): DependencyService => {
rootPathForConfig: string,
workspacePath: string,
useWorkspaceDependencies: boolean,
nodeModulesPaths: string[],
tsSDKPath?: string
) {
const nodeModulesPaths = useWorkspaceDependencies ? await createNodeModulesPaths(rootPathForConfig) : [];

const loadTypeScript = async (): Promise<Dependency<typeof ts>[]> => {
try {
if (useWorkspaceDependencies && tsSDKPath) {
Expand Down
7 changes: 6 additions & 1 deletion server/src/services/projectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { DocumentService } from './documentService';
import { VueInfoService } from './vueInfoService';

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

export async function createProjectService(
rootPathForConfig: string,
workspacePath: string,
projectPath: string,
tsconfigPath: string | undefined,
packagePath: string | undefined,
Expand Down Expand Up @@ -119,9 +119,14 @@ export async function createProjectService(
configure(initialConfig);

return {
rootPathForConfig,
configure,
languageModes,
async onDocumentFormatting({ textDocument, options }) {
if (!$config.vetur.format.enable) {
return [];
}

const doc = documentService.getDocument(textDocument.uri)!;

const modeRanges = languageModes.getAllLanguageModeRangesInDocument(doc);
Expand Down
178 changes: 98 additions & 80 deletions server/src/services/vls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ import {
CompletionParams,
ExecuteCommandParams,
ApplyWorkspaceEditRequest,
FoldingRangeParams
FoldingRangeParams,
DidChangeWorkspaceFoldersNotification
} from 'vscode-languageserver';
import {
ColorInformation,
CompletionItem,
CompletionList,
Definition,
Diagnostic,
DocumentHighlight,
DocumentLink,
Hover,
Expand All @@ -48,85 +48,68 @@ import {
import type { TextDocument } from 'vscode-languageserver-textdocument';

import { URI } from 'vscode-uri';
import { LanguageModes, LanguageModeRange, LanguageMode } from '../embeddedSupport/languageModes';
import { NULL_COMPLETION, NULL_HOVER, NULL_SIGNATURE } from '../modes/nullMode';
import { VueInfoService } from './vueInfoService';
import { createDependencyService, DependencyService } from './dependencyService';
import { createDependencyService, createNodeModulesPaths } from './dependencyService';
import _ from 'lodash';
import { DocumentContext, RefactorAction } from '../types';
import { RefactorAction } from '../types';
import { DocumentService } from './documentService';
import { VueHTMLMode } from '../modes/template';
import { logger } from '../log';
import { getDefaultVLSConfig, VLSFullConfig, VLSConfig, getVeturFullConfig, VeturFullConfig } from '../config';
import { LanguageId } from '../embeddedSupport/embeddedSupport';
import { getDefaultVLSConfig, VLSFullConfig, getVeturFullConfig, VeturFullConfig } from '../config';
import { APPLY_REFACTOR_COMMAND } from '../modes/script/javascript';
import { VCancellationToken, VCancellationTokenSource } from '../utils/cancellationToken';
import { findConfigFile } from '../utils/workspace';
import { createProjectService, ProjectService } from './projectService';

export class VLS {
// @Todo: Remove this and DocumentContext
private workspacePath: string | undefined;
private veturConfig: VeturFullConfig;
// private workspacePath: string | undefined;
private workspaces: Map<string, VeturFullConfig & { workspaceFsPath: string }>;
private nodeModulesMap: Map<string, string[]>;
// private veturConfig: VeturFullConfig;
private documentService: DocumentService;
private rootPathForConfig: string;
// private rootPathForConfig: string;
private globalSnippetDir: string;
private projects: Map<string, ProjectService>;
private dependencyService: DependencyService;
// private dependencyService: DependencyService;

private pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {};
private cancellationTokenValidationRequests: { [uri: string]: VCancellationTokenSource } = {};
private validationDelayMs = 200;
private validation: { [k: string]: boolean } = {
'vue-html': true,
html: true,
css: true,
scss: true,
less: true,
postcss: true,
javascript: true
};
private templateInterpolationValidation = false;

private documentFormatterRegistration: Disposable | undefined;

private config: VLSFullConfig;
private workspaceConfig: VLSFullConfig;

constructor(private lspConnection: Connection) {
this.documentService = new DocumentService(this.lspConnection);
this.dependencyService = createDependencyService();
this.projects = new Map();
this.nodeModulesMap = new Map();
}

async init(params: InitializeParams) {
const workspacePath = params.rootPath;
if (!workspacePath) {
let workspaceFolders =
!Array.isArray(params.workspaceFolders) && params.rootPath
? [{ name: '', fsPath: normalizeFileNameToFsPath(params.rootPath) }]
: params.workspaceFolders?.map(el => ({ name: el.name, fsPath: getFileFsPath(el.uri) })) ?? [];

if (workspaceFolders.length === 0) {
console.error('No workspace path found. Vetur initialization failed.');
return {
capabilities: {}
};
}

this.workspacePath = normalizeFileNameToFsPath(workspacePath);

this.workspaces = new Map();
this.globalSnippetDir = params.initializationOptions?.globalSnippetDir;
const veturConfigPath = findConfigFile(this.workspacePath, 'vetur.config.js');
this.rootPathForConfig = normalizeFileNameToFsPath(veturConfigPath ? path.dirname(veturConfigPath) : workspacePath);
this.veturConfig = await getVeturFullConfig(
this.rootPathForConfig,
this.workspacePath,
veturConfigPath ? require(veturConfigPath) : {}
);
const config = this.getFullConfig(params.initializationOptions?.config);

await this.dependencyService.init(
this.rootPathForConfig,
this.workspacePath,
config.vetur.useWorkspaceDependencies,
config.typescript.tsdk
);
this.projects = new Map();
await Promise.all(workspaceFolders.map(workspace => this.addWorkspace(workspace)));

this.workspaceConfig = this.getVLSFullConfig({}, params.initializationOptions?.config);

this.configure(config);
if (params.capabilities.workspace?.workspaceFolders) {
this.setupWorkspaceListeners();
}
this.setupConfigListeners();
this.setupLSPHandlers();
this.setupCustomLSPHandlers();
Expand All @@ -141,28 +124,70 @@ export class VLS {
this.lspConnection.listen();
}

private getFullConfig(config: any | undefined): VLSFullConfig {
private getVLSFullConfig(settings: VeturFullConfig['settings'], config: any | undefined): VLSFullConfig {
const result = config ? _.merge(getDefaultVLSConfig(), config) : getDefaultVLSConfig();
Object.keys(this.veturConfig.settings).forEach(key => {
_.set(result, key, this.veturConfig.settings[key]);
Object.keys(settings).forEach(key => {
_.set(result, key, settings[key]);
});
return result;
}

private async addWorkspace(workspace: { name: string; fsPath: string }) {
const veturConfigPath = findConfigFile(workspace.fsPath, 'vetur.config.js');
const rootPathForConfig = normalizeFileNameToFsPath(
veturConfigPath ? path.dirname(veturConfigPath) : workspace.fsPath
);
if (!this.workspaces.has(rootPathForConfig)) {
this.workspaces.set(rootPathForConfig, {
...(await getVeturFullConfig(
rootPathForConfig,
workspace.fsPath,
veturConfigPath ? require(veturConfigPath) : {}
)),
workspaceFsPath: workspace.fsPath
});
}
}

private setupWorkspaceListeners() {
this.lspConnection.client.register(DidChangeWorkspaceFoldersNotification.type);
this.lspConnection.workspace.onDidChangeWorkspaceFolders(async e => {
await Promise.all(e.added.map(el => this.addWorkspace({ name: el.name, fsPath: getFileFsPath(el.uri) })));
});
}

private setupConfigListeners() {
this.lspConnection.onDidChangeConfiguration(async ({ settings }: DidChangeConfigurationParams) => {
const config = this.getFullConfig(settings);
this.configure(config);
this.setupDynamicFormatters(config);
let isFormatEnable = false;
this.projects.forEach(project => {
const veturConfig = this.workspaces.get(project.rootPathForConfig);
if (!veturConfig) return;
const fullConfig = this.getVLSFullConfig(veturConfig.settings, settings);
project.configure(fullConfig);
isFormatEnable = isFormatEnable || fullConfig.vetur.format.enable;
});
this.setupDynamicFormatters(isFormatEnable);
});

this.documentService.getAllDocuments().forEach(this.triggerValidation);
}

private async getProjectService(uri: DocumentUri): Promise<ProjectService | undefined> {
const projectRootPaths = this.veturConfig.projects
const projectRootPaths = _.flatten(
Array.from(this.workspaces.entries()).map(([rootPathForConfig, veturConfig]) =>
veturConfig.projects.map(project => ({
...project,
rootPathForConfig,
vlsFullConfig: this.getVLSFullConfig(veturConfig.settings, this.workspaceConfig),
workspaceFsPath: veturConfig.workspaceFsPath
}))
)
)
.map(project => ({
rootFsPath: normalizeFileNameResolve(this.rootPathForConfig, project.root),
vlsFullConfig: project.vlsFullConfig,
rootPathForConfig: project.rootPathForConfig,
workspaceFsPath: project.workspaceFsPath,
rootFsPath: normalizeFileNameResolve(project.rootPathForConfig, project.root),
tsconfigPath: project.tsconfig,
packagePath: project.package,
snippetFolder: project.snippetFolder,
Expand All @@ -178,18 +203,31 @@ export class VLS {
return this.projects.get(projectConfig.rootFsPath);
}

const dependencyService = createDependencyService();
const nodeModulePaths =
this.nodeModulesMap.get(projectConfig.rootPathForConfig) ??
createNodeModulesPaths(projectConfig.rootPathForConfig);
if (this.nodeModulesMap.has(projectConfig.rootPathForConfig)) {
this.nodeModulesMap.set(projectConfig.rootPathForConfig, nodeModulePaths);
}
await dependencyService.init(
projectConfig.rootPathForConfig,
projectConfig.workspaceFsPath,
projectConfig.vlsFullConfig.vetur.useWorkspaceDependencies,
nodeModulePaths,
projectConfig.vlsFullConfig.typescript.tsdk
);
const project = await createProjectService(
this.rootPathForConfig,
this.workspacePath ?? this.rootPathForConfig,
projectConfig.rootPathForConfig,
projectConfig.rootFsPath,
projectConfig.tsconfigPath,
projectConfig.packagePath,
projectConfig.snippetFolder,
projectConfig.globalComponents,
this.documentService,
this.config,
this.workspaceConfig,
this.globalSnippetDir,
this.dependencyService
dependencyService
);
this.projects.set(projectConfig.rootFsPath, project);
return project;
Expand Down Expand Up @@ -232,8 +270,8 @@ export class VLS {
});
}

private async setupDynamicFormatters(settings: VLSFullConfig) {
if (settings.vetur.format.enable) {
private async setupDynamicFormatters(enable: boolean) {
if (enable) {
if (!this.documentFormatterRegistration) {
this.documentFormatterRegistration = await this.lspConnection.client.register(DocumentFormattingRequest.type, {
documentSelector: [{ language: 'vue' }]
Expand Down Expand Up @@ -270,26 +308,6 @@ export class VLS {
});
}

configure(config: VLSConfig): void {
this.config = config;

const veturValidationOptions = config.vetur.validation;
this.validation['vue-html'] = veturValidationOptions.template;
this.validation.css = veturValidationOptions.style;
this.validation.postcss = veturValidationOptions.style;
this.validation.scss = veturValidationOptions.style;
this.validation.less = veturValidationOptions.style;
this.validation.javascript = veturValidationOptions.script;

this.templateInterpolationValidation = config.vetur.experimental.templateInterpolationService;

this.projects.forEach(project => {
project.configure(config);
});

logger.setLevel(config.vetur.dev.logLevel);
}

/**
* Custom Notifications
*/
Expand Down Expand Up @@ -407,8 +425,7 @@ export class VLS {
this.cancelPastValidation(textDocument);
this.pendingValidationRequests[textDocument.uri] = setTimeout(() => {
delete this.pendingValidationRequests[textDocument.uri];
const tsDep = this.dependencyService.get('typescript');
this.cancellationTokenValidationRequests[textDocument.uri] = new VCancellationTokenSource(tsDep.module);
this.cancellationTokenValidationRequests[textDocument.uri] = new VCancellationTokenSource();
this.validateTextDocument(textDocument, this.cancellationTokenValidationRequests[textDocument.uri].token);
}, this.validationDelayMs);
}
Expand Down Expand Up @@ -470,6 +487,7 @@ export class VLS {
get capabilities(): ServerCapabilities {
return {
textDocumentSync: TextDocumentSyncKind.Incremental,
workspace: { workspaceFolders: { supported: true, changeNotifications: true } },
completionProvider: { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', "'", '/', '@', '*', ' '] },
signatureHelpProvider: { triggerCharacters: ['('] },
documentFormattingProvider: false,
Expand Down
7 changes: 1 addition & 6 deletions server/src/utils/cancellationToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,15 @@ export interface VCancellationToken extends LSPCancellationToken {
}

export class VCancellationTokenSource extends CancellationTokenSource {
constructor(private tsModule: RuntimeLibrary['typescript']) {
super();
}

get token(): VCancellationToken {
const operationCancelException = this.tsModule.OperationCanceledException;
const token = super.token as VCancellationToken;
token.tsToken = {
isCancellationRequested() {
return token.isCancellationRequested;
},
throwIfCancellationRequested() {
if (token.isCancellationRequested) {
throw new operationCancelException();
throw new Error('OperationCanceledException');
}
}
};
Expand Down

0 comments on commit 1481fd2

Please sign in to comment.