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

Migrate to Volar #294

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,274 changes: 739 additions & 2,535 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions packages/language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@
"prettier-plugin-marko": "^3.1.12",
"relative-import-path": "^1.0.0",
"typescript": "^5.7.2",
"@volar/kit": "^2.4.5",
"@volar/language-core": "^2.4.5",
"@volar/language-server": "^2.4.5",
"@volar/language-service": "^2.4.5",
"@volar/typescript": "^2.4.5",
"volar-service-css": "^0.0.62",
"volar-service-emmet": "^0.0.62",
"volar-service-html": "^0.0.62",
"volar-service-prettier": "^0.0.62",
"volar-service-typescript": "^0.0.62",
"volar-service-typescript-twoslash-queries": "^0.0.62",
"vscode-css-languageservice": "^6.3.2",
"vscode-languageserver": "^9.0.1",
"vscode-languageserver-textdocument": "^1.0.12",
Expand Down
54 changes: 31 additions & 23 deletions packages/language-server/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { Project } from "@marko/language-tools";
import fs from "fs";
import snapshot from "mocha-snap";
import path from "path";
import { CancellationToken, Position } from "vscode-languageserver";
import { Position } from "vscode-languageserver";
// import { bench, run } from "mitata";
import { TextDocument } from "vscode-languageserver-textdocument";
// import { bench, run } from "mitata";
import { URI } from "vscode-uri";

import MarkoLangaugeService, { documents } from "../service";
import { codeFrame } from "./util/code-frame";
import { getLanguageServer } from "./util/language-service";

Project.setDefaultTypePaths({
internalTypesFile: require.resolve(
Expand All @@ -21,38 +22,33 @@ Project.setDefaultTypePaths({
// const BENCHED = new Set<string>();
const FIXTURE_DIR = path.join(__dirname, "fixtures");

after(async () => {
const handle = await getLanguageServer();
await handle.shutdown();
});

for (const subdir of fs.readdirSync(FIXTURE_DIR)) {
const fixtureSubdir = path.join(FIXTURE_DIR, subdir);

if (!fs.statSync(fixtureSubdir).isDirectory()) continue;
for (const entry of fs.readdirSync(fixtureSubdir)) {
it(entry, async () => {
const serverHandle = await getLanguageServer();

const fixtureDir = path.join(fixtureSubdir, entry);

for (const filename of loadMarkoFiles(fixtureDir)) {
const doc = documents.get(URI.file(filename).toString())!;
const doc = await serverHandle.openTextDocument(filename, "marko");
const code = doc.getText();
const params = {
textDocument: {
uri: doc.uri,
languageId: doc.languageId,
version: doc.version,
text: code,
},
} as const;
documents.doOpen(params);

let results = "";

for (const position of getHovers(doc)) {
const hoverInfo = await MarkoLangaugeService.doHover(
doc,
{
position,
textDocument: doc,
},
CancellationToken.None,
const hoverInfo = await serverHandle.sendHoverRequest(
doc.uri,
position,
);

const loc = { start: position, end: position };

let message = "";
Expand Down Expand Up @@ -82,12 +78,24 @@ for (const subdir of fs.readdirSync(FIXTURE_DIR)) {
results = `## Hovers\n${results}`;
}

const errors = await MarkoLangaugeService.doValidate(doc);
const diagnosticReport =
await serverHandle.sendDocumentDiagnosticRequest(doc.uri);

if (errors && errors.length) {
if (
diagnosticReport.kind === "full" &&
diagnosticReport.items &&
diagnosticReport.items.length
) {
results += "## Diagnostics\n";

for (const error of errors) {
diagnosticReport.items.sort((a, b) => {
const lineDiff = a.range.start.line - b.range.start.line;
if (lineDiff === 0) {
return a.range.start.character - b.range.start.character;
}
return lineDiff;
});
for (const error of diagnosticReport.items) {
const loc = {
start: error.range.start,
end: error.range.end,
Expand All @@ -98,7 +106,7 @@ for (const subdir of fs.readdirSync(FIXTURE_DIR)) {
}
}

documents.doClose(params);
await serverHandle.closeTextDocument(doc.uri);

await snapshot(results, {
file: path.relative(fixtureDir, filename.replace(/\.marko$/, ".md")),
Expand Down
181 changes: 37 additions & 144 deletions packages/language-server/src/__tests__/util/language-service.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,22 @@
import type { Extracted } from "@marko/language-tools";
import { getExt } from "@marko/language-tools";
import {
createFSBackedSystem,
createVirtualLanguageServiceHost,
} from "@typescript/vfs";
import fs from "fs";
import path from "path";
import ts from "typescript";
import { LanguageServerHandle, startLanguageServer } from "@volar/test-utils";
import * as protocol from "vscode-languageserver-protocol/node";
import { Project } from "@marko/language-tools";

const rootDir = process.cwd();
const startPosition: ts.LineAndCharacter = {
line: 0,
character: 0,
};

export type Processors = Record<
string,
{
ext: ts.Extension;
kind: ts.ScriptKind;
extract(filename: string, code: string): Extracted;
}
>;
let serverHandle: LanguageServerHandle | undefined;

Project.setDefaultTypePaths({
internalTypesFile: require.resolve(
"@marko/language-tools/marko.internal.d.ts",
),
markoTypesFile: require.resolve("marko/index.d.ts"),
});

export function createLanguageService(
fsMap: Map<string, string>,
processors: Processors,
) {
const getProcessor = (filename: string) =>
processors[getExt(filename)?.slice(1) || ""];
export async function getLanguageServer() {
const compilerOptions: ts.CompilerOptions = {
...ts.getDefaultCompilerOptions(),
rootDir,
Expand All @@ -41,132 +29,37 @@ export function createLanguageService(
allowNonTsExtensions: true,
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ESNext,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
};
const rootFiles = [...fsMap.keys()];
const sys = createFSBackedSystem(fsMap, rootDir, ts);

const { languageServiceHost: lsh } = createVirtualLanguageServiceHost(
sys,
rootFiles,
compilerOptions,
ts,
);

const ls = ts.createLanguageService(lsh);
const snapshotCache = new Map<string, [Extracted, ts.IScriptSnapshot]>();

/**
* Trick TypeScript into thinking Marko files are TS/JS files.
*/
lsh.getScriptKind = (filename: string) => {
const processor = getProcessor(filename);
return processor ? processor.kind : ts.ScriptKind.TS;
moduleResolution: ts.ModuleResolutionKind.NodeNext,
};

/**
* A script snapshot is an immutable string of text representing the contents of a file.
* We patch it so that Marko files instead return their extracted ts code.
*/
const getScriptSnapshot = lsh.getScriptSnapshot!.bind(lsh);
lsh.getScriptSnapshot = (filename: string) => {
const processor = getProcessor(filename);
if (processor) {
let cached = snapshotCache.get(filename);
if (!cached) {
const extracted = processor.extract(
filename,
lsh.readFile(filename, "utf-8") || "",
);
snapshotCache.set(
filename,
(cached = [
extracted,
ts.ScriptSnapshot.fromString(extracted.toString()),
]),
);
}

return cached[1];
}

return getScriptSnapshot(filename);
};

/**
* This ensures that any directory reads with specific file extensions also include Marko.
* It is used for example when completing the `from` property of the `import` statement.
*/
const readDirectory = lsh.readDirectory!.bind(lsh);
const additionalExts = Object.keys(processors);
lsh.readDirectory = (path, extensions, exclude, include, depth) => {
return readDirectory(
path,
extensions?.concat(additionalExts),
exclude,
include,
depth,
if (!serverHandle) {
console.log("Starting language server");
console.log(" - bin, ", path.resolve("./bin.js"));
console.log(
" - Working Dir",
path.resolve(rootDir, "./__tests__/fixtures/"),
);
};
serverHandle = startLanguageServer(path.resolve("./bin.js"));

/**
* TypeScript doesn't know how to resolve `.marko` files.
* Below we first try to use TypeScripts normal resolution, and then fallback
* to seeing if a `.marko` file exists at the same location.
*/
lsh.resolveModuleNames = (moduleNames, containingFile) => {
const resolvedModules: (
| ts.ResolvedModuleFull
| ts.ResolvedModule
| undefined
)[] = moduleNames.map<ts.ResolvedModule | undefined>(
(moduleName) =>
ts.resolveModuleName(moduleName, containingFile, compilerOptions, sys)
.resolvedModule,
const tsdkPath = path.dirname(
require.resolve("typescript/lib/typescript.js"),
);
console.log(" - tsdkPath", tsdkPath);
await serverHandle.initialize(path.resolve("./"), {
typescript: {
tsdk: tsdkPath,
compilerOptions,
},
});

// Ensure that our first test does not suffer from a TypeScript overhead
await serverHandle.sendCompletionRequest(
"file://doesnt-exists",
protocol.Position.create(0, 0),
);
}

for (let i = resolvedModules.length; i--; ) {
if (!resolvedModules[i]) {
const moduleName = moduleNames[i];
const processor = moduleName[0] !== "*" && getProcessor(moduleName);
if (processor && moduleName[0] === ".") {
// For relative paths just see if it exists on disk.
const resolvedFileName = path.resolve(
containingFile,
"..",
moduleName,
);
if (lsh.fileExists(resolvedFileName)) {
resolvedModules[i] = {
resolvedFileName,
extension: processor.ext,
isExternalLibraryImport: false,
};
}
}
}
}

return resolvedModules;
};

/**
* Whenever TypeScript requests line/character info we return with the source
* file line/character if it exists.
*/
const toLineColumnOffset = ls.toLineColumnOffset!;
ls.toLineColumnOffset = (fileName, pos) => {
if (pos === 0) return startPosition;

const extracted = snapshotCache.get(fileName)?.[0];
if (extracted) {
return extracted.sourcePositionAt(pos) || startPosition;
}

return toLineColumnOffset(fileName, pos);
};

return ls;
return serverHandle;
}

export function loadMarkoFiles(dir: string, all = new Set<string>()) {
Expand Down
Loading