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

Import from anywhere #604

Merged
merged 20 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4831d0c
Use JayveeService instead of Langium specific one in utils
georg-schwarz Jul 26, 2024
f60e4e7
Add method findUnresolvedImportURIs to JayveeImportResolver
georg-schwarz Jul 26, 2024
11849b5
Add hook on workspace initialization to load unresolved documents out…
georg-schwarz Jul 26, 2024
a79bbd3
Initialize the workspace in the VSCode extension
georg-schwarz Jul 26, 2024
f619fe4
Dynamic file loading from fs as string instead from URI to allow brow…
georg-schwarz Jul 26, 2024
641d3be
Use FileSystemProvider instead of node:fs
georg-schwarz Jul 26, 2024
b9abbaa
Add launch config for electric vehicle example
georg-schwarz Jul 26, 2024
675c795
Add hooks before initializing the workspace
georg-schwarz Jul 26, 2024
1da7dc2
Use mutex to prevent adding documents multiple times
georg-schwarz Jul 26, 2024
835f9ca
Only load documents with correct file extension
georg-schwarz Jul 26, 2024
9a93a06
Fix error message when import cannot be resolved
georg-schwarz Jul 26, 2024
2803955
Refactor check on correct file extension before loading
georg-schwarz Jul 26, 2024
d942134
Remove mutex due to deadlock
georg-schwarz Jul 26, 2024
4ce82e2
Add tests for the dynamic import
georg-schwarz Jul 26, 2024
74a5ae0
Place more narrow mutex lock around adding new documents
georg-schwarz Jul 29, 2024
25274d6
Rename std-lib directory to workspace
georg-schwarz Jul 29, 2024
86a66f7
Extract dynamic file import to own file and initialize while creating…
georg-schwarz Jul 29, 2024
4d4b10c
Add license text
georg-schwarz Jul 29, 2024
add26e4
Fix copyright year
georg-schwarz Jul 30, 2024
77c608c
Fix typo in test file name
georg-schwarz Jul 30, 2024
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
6 changes: 6 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
"request": "launch",
"type": "node-terminal"
},
{
"name": "Run Electric Vehicles Example",
"command": "npm run example:vehicles",
"request": "launch",
"type": "node-terminal"
},
{
"name": "Run Gas Reserve Example",
"command": "npm run example:gas",
Expand Down
100 changes: 95 additions & 5 deletions apps/vs-code-extension/src/language-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,109 @@
//
// SPDX-License-Identifier: AGPL-3.0-only

import { createJayveeServices } from '@jvalue/jayvee-language-server';
import { startLanguageServer } from 'langium/lsp';
import {
type JayveeServices,
createJayveeServices,
initializeWorkspace,
} from '@jvalue/jayvee-language-server';
import {
type LangiumSharedServices,
addCallHierarchyHandler,
addCodeActionHandler,
addCodeLensHandler,
addCompletionHandler,
addConfigurationChangeHandler,
addDiagnosticsHandler,
addDocumentHighlightsHandler,
addDocumentLinkHandler,
addDocumentSymbolHandler,
addDocumentUpdateHandler,
addExecuteCommandHandler,
addFileOperationHandler,
addFindReferencesHandler,
addFoldingRangeHandler,
addFormattingHandler,
addGoToDeclarationHandler,
addGoToImplementationHandler,
addGoToTypeDefinitionHandler,
addGotoDefinitionHandler,
addHoverHandler,
addInlayHintHandler,
addRenameHandler,
addSemanticTokenHandler,
addSignatureHelpHandler,
addTypeHierarchyHandler,
addWorkspaceSymbolHandler,
} from 'langium/lsp';
import { NodeFileSystem } from 'langium/node';
import { ProposedFeatures, createConnection } from 'vscode-languageserver/node';

// Create a connection to the client
const connection = createConnection(ProposedFeatures.all);

// Inject the shared services and language-specific services
const { shared } = createJayveeServices({
const services = createJayveeServices({
connection,
...NodeFileSystem,
});

// Start the language server with the shared services
startLanguageServer(shared);
startLanguageServer(services.shared, services.Jayvee);

/**
* Starts the language server and registers hooks.
* Adapted from 'langium/lsp' to initialize workspace correctly.
*/
function startLanguageServer(
shared: LangiumSharedServices,
jayvee: JayveeServices,
): void {
const connection = shared.lsp.Connection;
if (!connection) {
throw new Error(
'Starting a language server requires the languageServer.Connection service to be set.',
);
}

addDocumentUpdateHandler(connection, shared);
addFileOperationHandler(connection, shared);
addDiagnosticsHandler(connection, shared);
addCompletionHandler(connection, shared);
addFindReferencesHandler(connection, shared);
addDocumentSymbolHandler(connection, shared);
addGotoDefinitionHandler(connection, shared);
addGoToTypeDefinitionHandler(connection, shared);
addGoToImplementationHandler(connection, shared);
addDocumentHighlightsHandler(connection, shared);
addFoldingRangeHandler(connection, shared);
addFormattingHandler(connection, shared);
addCodeActionHandler(connection, shared);
addRenameHandler(connection, shared);
addHoverHandler(connection, shared);
addInlayHintHandler(connection, shared);
addSemanticTokenHandler(connection, shared);
addExecuteCommandHandler(connection, shared);
addSignatureHelpHandler(connection, shared);
addCallHierarchyHandler(connection, shared);
addTypeHierarchyHandler(connection, shared);
addCodeLensHandler(connection, shared);
addDocumentLinkHandler(connection, shared);
addConfigurationChangeHandler(connection, shared);
addGoToDeclarationHandler(connection, shared);
addWorkspaceSymbolHandler(connection, shared);

connection.onInitialize(async (params) => {
const initResult = shared.lsp.LanguageServer.initialize(params);
await initializeWorkspace(jayvee, []); // Initialize workspace to register dynamic document loading hooks
return initResult;
});
connection.onInitialized((params) => {
shared.lsp.LanguageServer.initialized(params);
});

// Make the text document manager listen on the connection for open, change and close text document events.
const documents = shared.workspace.TextDocuments;
documents.listen(connection);

// Start listening for incoming messages from the client.
connection.listen();
}
11 changes: 7 additions & 4 deletions libs/interpreter-lib/src/parsing-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import * as fs from 'node:fs';
import path from 'node:path';

import { type Logger } from '@jvalue/jayvee-execution';
import { initializeWorkspace } from '@jvalue/jayvee-language-server';
import {
type JayveeServices,
initializeWorkspace,
} from '@jvalue/jayvee-language-server';
import { type AstNode, type LangiumDocument } from 'langium';
import { type LangiumServices } from 'langium/lsp';
import { DiagnosticSeverity } from 'vscode-languageserver-protocol';
Expand Down Expand Up @@ -58,7 +61,7 @@ export async function extractDocumentFromFile(
*/
export async function extractDocumentFromString(
modelString: string,
services: LangiumServices,
services: JayveeServices,
logger: Logger,
): Promise<LangiumDocument> {
const document = services.shared.workspace.LangiumDocumentFactory.fromString(
Expand Down Expand Up @@ -104,7 +107,7 @@ export async function validateDocument(

export async function extractAstNodeFromFile<T extends AstNode>(
filePath: string,
services: LangiumServices,
services: JayveeServices,
logger: Logger,
): Promise<T> {
return (await extractDocumentFromFile(filePath, services, logger)).parseResult
Expand All @@ -113,7 +116,7 @@ export async function extractAstNodeFromFile<T extends AstNode>(

export async function extractAstNodeFromString<T extends AstNode>(
modelString: string,
services: LangiumServices,
services: JayveeServices,
logger: Logger,
): Promise<T> {
return (await extractDocumentFromString(modelString, services, logger))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@

import {
DefaultWorkspaceManager,
type LangiumCoreServices,
DocumentState,
type LangiumDocument,
type LangiumDocumentFactory,
type LangiumSharedCoreServices,
} from 'langium';
import { type LangiumSharedServices } from 'langium/lsp';
import { type WorkspaceFolder } from 'vscode-languageserver';
import { URI } from 'vscode-uri';

import { type JayveeModel, isJayveeModel } from '../ast';
import { type JayveeServices } from '../jayvee-module';

import { getStdLib } from './stdlib';

export class JayveeWorkspaceManager extends DefaultWorkspaceManager {
private documentFactory: LangiumDocumentFactory;

constructor(services: LangiumSharedCoreServices) {
constructor(services: LangiumSharedServices) {
super(services);
this.documentFactory = services.workspace.LangiumDocumentFactory;
}
Expand All @@ -39,10 +42,75 @@ export class JayveeWorkspaceManager extends DefaultWorkspaceManager {
* Also loads additional required files, e.g., the standard library
*/
export async function initializeWorkspace(
services: LangiumCoreServices,
services: JayveeServices,
workspaceFolders: WorkspaceFolder[] = [],
): Promise<void> {
addCollectUnresolvedImportHook(services);

await services.shared.workspace.WorkspaceManager.initializeWorkspace(
workspaceFolders,
);
}

function addCollectUnresolvedImportHook(services: JayveeServices): void {
const documentBuilder = services.shared.workspace.DocumentBuilder;
const importResolver = services.ImportResolver;

documentBuilder.onBuildPhase(DocumentState.IndexedContent, async (docs) => {
for (const doc of docs) {
const model = doc.parseResult.value;
if (!isJayveeModel(model)) {
return;
}
await services.shared.workspace.WorkspaceLock.write(async () => {
const importURIs = importResolver.findUnresolvedImportURIs(model);

for (const importURI of importURIs) {
await loadDocumentFromFs(importURI, services);
}
});
}
});
}

async function loadDocumentFromFs(
importURI: URI,
services: JayveeServices,
): Promise<void> {
const allowedFileExtensions = services.shared.ServiceRegistry.all.flatMap(
(e) => e.LanguageMetaData.fileExtensions,
);
const fileExtension = importURI.fsPath.split('.').at(-1);
if (
fileExtension === undefined ||
!allowedFileExtensions.includes(fileExtension)
) {
return;
}

const langiumDocuments = services.shared.workspace.LangiumDocuments;
const documentBuilder = services.shared.workspace.DocumentBuilder;
const documentFactory = services.shared.workspace.LangiumDocumentFactory;

const file = await loadFileFromUri(importURI, services);
if (file === undefined) {
return;
}

const document = documentFactory.fromString<JayveeModel>(file, importURI);
await documentBuilder.build([document], { validation: true });
langiumDocuments.addDocument(document);
}

async function loadFileFromUri(
uri: URI,
services: JayveeServices,
): Promise<string | undefined> {
const fileSystemProvider = services.shared.workspace.FileSystemProvider;

try {
return await fileSystemProvider.readFile(uri);
} catch (e) {
return undefined;
}
}
19 changes: 19 additions & 0 deletions libs/language-server/src/lib/services/import-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,25 @@ export class JayveeImportResolver {
this.availableElementsPerDocumentCache = new DocumentCache(services.shared);
}

/**
* Finds all import URIs that could not be resolved.
*/
findUnresolvedImportURIs(model: JayveeModel): URI[] {
const unresolvedURIs: URI[] = [];
for (const importDefinition of model.imports) {
const uri = this.resolveImportUri(importDefinition);
if (uri === undefined) {
continue;
}

const isDocumentResolved = this.documents.getDocument(uri) !== undefined;
if (!isDocumentResolved) {
unresolvedURIs.push(uri);
}
}
return unresolvedURIs;
}

resolveImport(importDefinition: ImportDefinition): JayveeModel | undefined {
const resolvedDocument = this.resolveImportedDocument(importDefinition);
if (resolvedDocument === undefined) {
Expand Down
6 changes: 3 additions & 3 deletions libs/language-server/src/test/langium-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@
* https://github.com/langium/langium/blob/main/packages/langium/src/test/langium-test.ts
*/
import { type AstNode, type BuildOptions, type LangiumDocument } from 'langium';
import { type LangiumServices } from 'langium/lsp';
import { type Diagnostic } from 'vscode-languageserver';
import { URI } from 'vscode-uri';

import { type JayveeServices } from '../lib';
import { initializeWorkspace } from '../lib/builtin-library/jayvee-workspace-manager';

export interface ParseHelperOptions extends BuildOptions {
documentUri?: string;
}

export function parseHelper<T extends AstNode = AstNode>(
services: LangiumServices,
services: JayveeServices,
): (
input: string,
options?: ParseHelperOptions,
Expand Down Expand Up @@ -49,7 +49,7 @@ export interface ValidationResult<T extends AstNode = AstNode> {
}

export function validationHelper<T extends AstNode = AstNode>(
services: LangiumServices,
services: JayveeServices,
): (input: string) => Promise<ValidationResult<T>> {
const parse = parseHelper<T>(services);
return async (input) => {
Expand Down
Loading