Skip to content

Commit

Permalink
refactor: more accurate interface design and terminology (#154)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnsoncodehk committed Mar 23, 2024
1 parent 1b7f456 commit 69f697e
Show file tree
Hide file tree
Showing 44 changed files with 1,009 additions and 950 deletions.
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

0 comments on commit 69f697e

Please sign in to comment.