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

refactor: more accurate interface design and terminology #154

Merged
merged 5 commits into from Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
49 changes: 24 additions & 25 deletions packages/kit/lib/createChecker.ts
@@ -1,19 +1,20 @@
import { CodeActionTriggerKind, Diagnostic, DiagnosticSeverity, DidChangeWatchedFilesParams, FileChangeType, LanguagePlugin, NotificationHandler, ServicePlugin, ServiceEnvironment, createLanguageService, mergeWorkspaceEdits, resolveCommonLanguageId, TypeScriptProjectHost } from '@volar/language-service';
import { CodeActionTriggerKind, Diagnostic, DiagnosticSeverity, DidChangeWatchedFilesParams, FileChangeType, LanguagePlugin, NotificationHandler, LanguageServicePlugin, ServiceEnvironment, createLanguageService, mergeWorkspaceEdits, resolveCommonLanguageId, TypeScriptProjectHost } from '@volar/language-service';
import * as path from 'typesafe-path/posix';
import * as ts from 'typescript';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { createServiceEnvironment } from './createServiceEnvironment';
import { asPosix, defaultCompilerOptions, fileNameToUri, uriToFileName } from './utils';
import { createLanguage } from '@volar/typescript';
import { createTypeScriptLanguage } from '@volar/typescript';

export function createTypeScriptChecker(
languages: LanguagePlugin[],
services: ServicePlugin[],
services: LanguageServicePlugin[],
tsconfig: string,
) {
const tsconfigPath = asPosix(tsconfig);
return createTypeScriptCheckerWorker(languages, services, tsconfigPath, env => {
return createTypeScriptCheckerWorker(languages, services, env => {
return createTypeScriptLanguageHost(
tsconfigPath,
env,
() => {
const parsed = ts.parseJsonSourceFileConfigFileContent(
Expand All @@ -34,12 +35,13 @@ export function createTypeScriptChecker(

export function createTypeScriptInferredChecker(
languages: LanguagePlugin[],
services: ServicePlugin[],
services: LanguageServicePlugin[],
getScriptFileNames: () => string[],
compilerOptions = defaultCompilerOptions
) {
return createTypeScriptCheckerWorker(languages, services, undefined, env => {
return createTypeScriptCheckerWorker(languages, services, env => {
return createTypeScriptLanguageHost(
undefined,
env,
() => ({
options: compilerOptions,
Expand All @@ -51,8 +53,7 @@ export function createTypeScriptInferredChecker(

function createTypeScriptCheckerWorker(
languages: LanguagePlugin[],
services: ServicePlugin[],
configFileName: string | undefined,
services: LanguageServicePlugin[],
getProjectHost: (env: ServiceEnvironment) => TypeScriptProjectHost
) {

Expand All @@ -70,17 +71,10 @@ function createTypeScriptCheckerWorker(
};
};

const languageHost = getProjectHost(env);
const language = createLanguage(
const language = createTypeScriptLanguage(
ts,
ts.sys,
languages,
configFileName,
languageHost,
{
fileNameToFileId: env.typescript!.fileNameToUri,
fileIdToFileName: env.typescript!.uriToFileName,
},
getProjectHost(env),
);
const service = createLanguageService(
language,
Expand All @@ -93,7 +87,7 @@ function createTypeScriptCheckerWorker(
check,
fixErrors,
printErrors,
languageHost,
language,

// settings
get settings() {
Expand Down Expand Up @@ -131,9 +125,9 @@ function createTypeScriptCheckerWorker(
async function fixErrors(fileName: string, diagnostics: Diagnostic[], only: string[] | undefined, writeFile: (fileName: string, newText: string) => Promise<void>) {
fileName = asPosix(fileName);
const uri = fileNameToUri(fileName);
const sourceFile = service.context.language.files.get(uri);
if (sourceFile) {
const document = service.context.documents.get(uri, sourceFile.languageId, sourceFile.snapshot);
const sourceScript = service.context.language.scripts.get(uri);
if (sourceScript) {
const document = service.context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot);
const range = { start: document.positionAt(0), end: document.positionAt(document.getText().length) };
const codeActions = await service.doCodeActions(uri, range, { diagnostics, only, triggerKind: 1 satisfies typeof CodeActionTriggerKind.Invoked });
if (codeActions) {
Expand All @@ -147,7 +141,7 @@ function createTypeScriptCheckerWorker(
for (const uri in rootEdit.changes ?? {}) {
const edits = rootEdit.changes![uri];
if (edits.length) {
const editFile = service.context.language.files.get(uri);
const editFile = service.context.language.scripts.get(uri);
if (editFile) {
const editDocument = service.context.documents.get(uri, editFile.languageId, editFile.snapshot);
const newString = TextDocument.applyEdits(editDocument, edits);
Expand All @@ -157,7 +151,7 @@ function createTypeScriptCheckerWorker(
}
for (const change of rootEdit.documentChanges ?? []) {
if ('textDocument' in change) {
const editFile = service.context.language.files.get(change.textDocument.uri);
const editFile = service.context.language.scripts.get(change.textDocument.uri);
if (editFile) {
const editDocument = service.context.documents.get(change.textDocument.uri, editFile.languageId, editFile.snapshot);
const newString = TextDocument.applyEdits(editDocument, change.edits);
Expand All @@ -182,8 +176,8 @@ function createTypeScriptCheckerWorker(
function formatErrors(fileName: string, diagnostics: Diagnostic[], rootPath: string) {
fileName = asPosix(fileName);
const uri = fileNameToUri(fileName);
const sourceFile = service.context.language.files.get(uri)!;
const document = service.context.documents.get(uri, sourceFile.languageId, sourceFile.snapshot);
const sourceScript = service.context.language.scripts.get(uri)!;
const document = service.context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot);
const errors: ts.Diagnostic[] = diagnostics.map<ts.Diagnostic>(diagnostic => ({
category: diagnostic.severity === 1 satisfies typeof DiagnosticSeverity.Error ? ts.DiagnosticCategory.Error : ts.DiagnosticCategory.Warning,
code: diagnostic.code as number,
Expand All @@ -202,6 +196,7 @@ function createTypeScriptCheckerWorker(
}

function createTypeScriptLanguageHost(
configFileName: string | undefined,
env: ServiceEnvironment,
createParsedCommandLine: () => Pick<ts.ParsedCommandLine, 'options' | 'fileNames'>
) {
Expand All @@ -212,6 +207,8 @@ function createTypeScriptLanguageHost(
let shouldCheckRootFiles = false;

const host: TypeScriptProjectHost = {
...ts.sys,
configFileName,
getCurrentDirectory: () => {
return uriToFileName(env.workspaceFolder);
},
Expand Down Expand Up @@ -239,6 +236,8 @@ function createTypeScriptLanguageHost(
return scriptSnapshotsCache.get(fileName);
},
getLanguageId: resolveCommonLanguageId,
fileNameToScriptId: env.typescript!.fileNameToUri,
scriptIdToFileName: env.typescript!.uriToFileName,
};

env.onDidChangeWatchedFiles?.(({ changes }) => {
Expand Down
10 changes: 5 additions & 5 deletions packages/kit/lib/createFormatter.ts
@@ -1,20 +1,20 @@
import { FormattingOptions, LanguagePlugin, ServicePlugin, createFileRegistry, createLanguageService } from '@volar/language-service';
import { FormattingOptions, LanguagePlugin, LanguageServicePlugin, createLanguage, createLanguageService } from '@volar/language-service';
import * as ts from 'typescript';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { createServiceEnvironment } from './createServiceEnvironment';

export function createFormatter(
languages: LanguagePlugin[],
services: ServicePlugin[]
services: LanguageServicePlugin[]
) {

let fakeUri = 'file:///dummy.txt';
let settings = {};

const env = createServiceEnvironment(() => settings);
const files = createFileRegistry(languages, false, () => { });
const language = createLanguage(languages, false, () => { });
const service = createLanguageService(
{ files },
language,
services,
env,
);
Expand All @@ -33,7 +33,7 @@ export function createFormatter(
async function format(content: string, languageId: string, options: FormattingOptions): Promise<string> {

const snapshot = ts.ScriptSnapshot.fromString(content);
files.set(fakeUri, languageId, snapshot);
language.scripts.set(fakeUri, languageId, snapshot);

const document = service.context.documents.get(fakeUri, languageId, snapshot);
const edits = await service.format(fakeUri, options, undefined, undefined);
Expand Down
161 changes: 159 additions & 2 deletions packages/language-core/index.ts
@@ -1,9 +1,166 @@
export * from '@volar/source-map';
export * from './lib/editorFeatures';
export * from './lib/fileRegistry';
export * from './lib/linkedCodeMap';
export * from './lib/types';
export * from './lib/utils';
export * from '@volar/source-map';

import { SourceMap } from '@volar/source-map';
import type * as ts from 'typescript';
import { LinkedCodeMap } from './lib/linkedCodeMap';
import type { CodeInformation, Language, LanguagePlugin, SourceScript, VirtualCode } from './lib/types';
import { FileMap } from './lib/utils';

export function createLanguage(plugins: LanguagePlugin[], caseSensitive: boolean, sync: (id: string) => void): Language {

const sourceScripts = new FileMap<SourceScript>(caseSensitive);
const virtualCodeToSourceFileMap = new WeakMap<VirtualCode, SourceScript>();
const virtualCodeToMaps = new WeakMap<ts.IScriptSnapshot, Map<string, [ts.IScriptSnapshot, SourceMap<CodeInformation>]>>();
const virtualCodeToLinkedCodeMap = new WeakMap<ts.IScriptSnapshot, LinkedCodeMap | undefined>();

return {
plugins,
scripts: {
get(id) {
sync(id);
return sourceScripts.get(id);
},
set(id, languageId, snapshot, _plugins = plugins) {
if (sourceScripts.has(id)) {
const sourceScript = sourceScripts.get(id)!;
if (sourceScript.languageId !== languageId) {
// languageId changed
this.delete(id);
return this.set(id, languageId, snapshot);
}
else if (sourceScript.snapshot !== snapshot) {
// snapshot updated
sourceScript.snapshot = snapshot;
if (sourceScript.generated) {
sourceScript.generated.root = sourceScript.generated.languagePlugin.updateVirtualCode(id, sourceScript.generated.root, snapshot);
sourceScript.generated.embeddedCodes.clear();
for (const code of forEachEmbeddedCode(sourceScript.generated.root)) {
virtualCodeToSourceFileMap.set(code, sourceScript);
sourceScript.generated.embeddedCodes.set(code.id, code);
}
}
return sourceScript;
}
else {
// not changed
return sourceScript;
}
}
else {
// created
const sourceScript: SourceScript = { id, languageId, snapshot };
sourceScripts.set(id, sourceScript);
for (const languagePlugin of _plugins) {
const virtualCode = languagePlugin.createVirtualCode(id, languageId, snapshot);
if (virtualCode) {
sourceScript.generated = {
root: virtualCode,
languagePlugin,
embeddedCodes: new Map(),
};
for (const code of forEachEmbeddedCode(virtualCode)) {
virtualCodeToSourceFileMap.set(code, sourceScript);
sourceScript.generated.embeddedCodes.set(code.id, code);
}
break;
}
}
return sourceScript;
}
},
delete(id) {
const value = sourceScripts.get(id);
if (value) {
if (value.generated) {
value.generated.languagePlugin.disposeVirtualCode?.(id, value.generated.root);
}
sourceScripts.delete(id);
}
},
},
maps: {
get(virtualCode, scriptId) {
if (!scriptId) {
const sourceScript = virtualCodeToSourceFileMap.get(virtualCode);
if (!sourceScript) {
return;
}
scriptId = sourceScript.id;
}
for (const [id, [_snapshot, map]] of this.forEach(virtualCode)) {
if (id === scriptId) {
return map;
}
}
},
forEach(virtualCode) {
let map = virtualCodeToMaps.get(virtualCode.snapshot);
if (!map) {
map = new Map();
virtualCodeToMaps.set(virtualCode.snapshot, map);
}
updateVirtualCodeMapOfMap(virtualCode, map, id => {
if (id) {
const sourceScript = sourceScripts.get(id)!;
return [id, sourceScript.snapshot];
}
else {
const sourceScript = virtualCodeToSourceFileMap.get(virtualCode)!;
return [sourceScript.id, sourceScript.snapshot];
}
});
return map;
},
},
linkedCodeMaps: {
get(virtualCode) {
if (!virtualCodeToLinkedCodeMap.has(virtualCode.snapshot)) {
virtualCodeToLinkedCodeMap.set(
virtualCode.snapshot,
virtualCode.linkedCodeMappings
? new LinkedCodeMap(virtualCode.linkedCodeMappings)
: undefined
);
}
return virtualCodeToLinkedCodeMap.get(virtualCode.snapshot);
},
},
};
}

export function updateVirtualCodeMapOfMap(
virtualCode: VirtualCode,
mapOfMap: Map<string, [ts.IScriptSnapshot, SourceMap<CodeInformation>]>,
getSourceSnapshot: (id: string | undefined) => [string, ts.IScriptSnapshot] | undefined,
) {
const sources = new Set<string | undefined>();
for (const mapping of virtualCode.mappings) {
if (sources.has(mapping.source)) {
continue;
}
sources.add(mapping.source);
const source = getSourceSnapshot(mapping.source);
if (!source) {
continue;
}
if (!mapOfMap.has(source[0]) || mapOfMap.get(source[0])![0] !== source[1]) {
mapOfMap.set(source[0], [source[1], new SourceMap(virtualCode.mappings.filter(mapping2 => mapping2.source === mapping.source))]);
}
}
}

export function* forEachEmbeddedCode(virtualCode: VirtualCode): Generator<VirtualCode> {
yield virtualCode;
if (virtualCode.embeddedCodes) {
for (const embeddedCode of virtualCode.embeddedCodes) {
yield* forEachEmbeddedCode(embeddedCode);
}
}
}

export function resolveCommonLanguageId(fileNameOrUri: string) {
const ext = fileNameOrUri.split('.').pop()!;
Expand Down