Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support for generating virtual file from multiple sources #2253

Merged
merged 10 commits into from Dec 24, 2022

Conversation

johnsoncodehk
Copy link
Member

@johnsoncodehk johnsoncodehk commented Dec 24, 2022

This PR is support for DSL that generates a virtual file from multiple source inputs.

Example

For the following source files:

// foo.data
const num = 1;
// foo.logic
console.log(num);

To generate the following virtual files:

// foo.data.ts
const num = 1;
// foo.logic.ts
const num = 1;
console.log(num);

Language Module:

import { LanguageModule, VirtualFile, FileKind, FileRangeCapabilities } from '@volar/language-core';
import type * as ts from 'typescript/lib/tsserverlibrary';
import type { Mapping } from '@volar/source-map';

class DataFile implements VirtualFile {

	static extension = '.data';

	fileName: string;
	kind = FileKind.TypeScriptHostFile;
	capabilities = {
		diagnostic: true,
		foldingRange: true,
		documentFormatting: true,
		documentSymbol: true,
		codeAction: true,
		inlayHint: true,
	};
	embeddedFiles: VirtualFile[] = [];
	mappings!: Mapping<FileRangeCapabilities>[];

	constructor(
		public sourceFileName: string,
		public snapshot: ts.IScriptSnapshot,
	) {
		this.fileName = sourceFileName + '.ts';
		this.update(snapshot);
	}

	public update(snapshot: ts.IScriptSnapshot) {
		this.snapshot = snapshot;
		this.mappings = [{
			sourceRange: [0, snapshot.getLength()],
			generatedRange: [0, snapshot.getLength()],
			data: {
				hover: true,
				references: true,
				definition: true,
				rename: true,
				completion: true,
				diagnostic: true,
				semanticTokens: true,
			},
		}];
	}
}

class LogicFile implements VirtualFile {

	static extension = '.logic';

	fileName: string;
	kind = FileKind.TypeScriptHostFile;
	capabilities = {
		diagnostic: true,
		foldingRange: true,
		documentFormatting: true,
		documentSymbol: true,
		codeAction: true,
		inlayHint: true,
	};
	embeddedFiles: VirtualFile[] = [];
	mappings!: Mapping<FileRangeCapabilities>[];
	snapshot!: ts.IScriptSnapshot;

	constructor(
		public sourceFileName: string,
		public dataSnapshot: ts.IScriptSnapshot | undefined,
		public logicSnapshot: ts.IScriptSnapshot,
	) {
		this.fileName = sourceFileName + '.ts';
		this.update(dataSnapshot, logicSnapshot);
	}

	public update(
		dataSnapshot: ts.IScriptSnapshot | undefined,
		logicSnapshot: ts.IScriptSnapshot,
	) {

		this.dataSnapshot = dataSnapshot;
		this.logicSnapshot = logicSnapshot;

		let text = '';
		if (this.dataSnapshot) {
			text += this.dataSnapshot.getText(0, this.dataSnapshot.getLength());
		}
		const logicStart = text.length;
		text += this.logicSnapshot.getText(0, this.logicSnapshot.getLength());

		this.snapshot = {
			getText: (start, end) => text.substring(start, end),
			getLength: () => text.length,
			getChangeRange: () => undefined,
		};
		this.mappings = [];
		if (this.dataSnapshot) {
			this.mappings.push({
				source: this.sourceFileName.replace(LogicFile.extension, DataFile.extension),
				sourceRange: [0, this.dataSnapshot.getLength()],
				generatedRange: [0, this.dataSnapshot.getLength()],
				data: {
					hover: true,
					references: true,
					definition: true,
					rename: true,
					completion: true,
					diagnostic: true,
					semanticTokens: true,
				},
			});
		}
		this.mappings.push({
			sourceRange: [0, this.logicSnapshot.getLength()],
			generatedRange: [logicStart, logicStart + this.logicSnapshot.getLength()],
			data: {
				hover: true,
				references: true,
				definition: true,
				rename: true,
				completion: true,
				diagnostic: true,
				semanticTokens: true,
			},
		});
	}
}

class MyLanguageModule implements LanguageModule<DataFile | LogicFile> {

	dataFiles = new Map<string, DataFile>();
	logicFiles = new Map<string, LogicFile>();

	constructor() { }

	createFile(fileName: string, snapshot: ts.IScriptSnapshot) {
		if (fileName.endsWith(DataFile.extension)) {
			const dataFile = new DataFile(fileName, snapshot);
			this.dataFiles.set(fileName, dataFile);
			this.onDataFileUpdated(dataFile, false);
			return dataFile;
		}
		if (fileName.endsWith(LogicFile.extension)) {
			const dataFile = this.dataFiles.get(fileName.replace(LogicFile.extension, DataFile.extension));
			const logicFile = new LogicFile(fileName, dataFile?.snapshot, snapshot);
			this.logicFiles.set(fileName, logicFile);
			return logicFile;
		}
	}

	updateFile(file: DataFile | LogicFile, snapshot: ts.IScriptSnapshot) {
		if (file instanceof DataFile) {
			file.update(snapshot);
			this.onDataFileUpdated(file, false);
		}
		if (file instanceof LogicFile) {
			file.update(file.dataSnapshot, snapshot);
		}
	}

	deleteFile(file: DataFile | LogicFile) {
		if (file instanceof DataFile) {
			this.dataFiles.delete(file.fileName);
			this.onDataFileUpdated(file, true);
		}
		if (file instanceof LogicFile) {
			this.logicFiles.delete(file.fileName);
		}
	}

	onDataFileUpdated(dataFile: DataFile, isDelete: boolean) {
		const logicFile = this.logicFiles.get(dataFile.sourceFileName.replace(DataFile.extension, LogicFile.extension));
		if (logicFile) {
			logicFile.update(isDelete ? undefined : dataFile.snapshot, logicFile.logicSnapshot);
		}
	}
}

@johnsoncodehk johnsoncodehk merged commit 2bb908d into master Dec 24, 2022
@johnsoncodehk johnsoncodehk deleted the multiple-sources branch December 24, 2022 18:39
@PupilTong
Copy link

This is the way I'm using. Great upgrade!

@PupilTong
Copy link

BTW. Which release will include this change?

@johnsoncodehk
Copy link
Member Author

It appeared in v1.0.17.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants