Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
67 changes: 0 additions & 67 deletions .github/.copilot.instructions.md

This file was deleted.

74 changes: 74 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copilot Instructions
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a typo, or did the naming convention change (i.e. the missing leading dot)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think previously you'd configure the path(s) to instructions using settings, and then they switched it to be convention based by default. i created the original file - this one was created with the agency /init switch.


## Build, Test, and Lint

This is a multi-package monorepo (root, `client/`, `server/`, `scripts/`). Each has its own `node_modules` and `package.json`. The root `npm install` runs `npm install-clean` in all sub-packages via `postinstall`.

```shell
npm install # install all packages (root + client + server + scripts)
npm run build # TypeScript compile all packages
npm run lint # ESLint all packages

# Server unit tests (mocha, no build dependency)
npm run test:server

# Client UI tests (vscode-test-electron, requires webpack first)
npm run webpack-prod
npm run test:client

# Run a single server test file directly
cd server
npx mocha --require ts-node/register src/test/<file>.test.ts

# Package the extension
npm run vsix
```

## Architecture

This is a VS Code extension providing Language Server Protocol (LSP) support for the Power Query / M formula language.

**Client** (`client/src/extension.ts`): Activates the extension, starts the language server over **Node IPC**, and manages the library symbol system. Exposes a `PowerQueryApi` for other extensions.

**Server** (`server/src/server.ts`): Handles LSP requests — completion, hover, definition, formatting, diagnostics, rename, folding, document symbols, semantic tokens, and signature help. Request handling follows a consistent pattern: fetch document → create cancellation token → build a `PQLS.Analysis` → call the analysis API → map results to LSP types. Errors go through `ErrorUtils.handleError`.

**Scripts** (`scripts/`): Standalone benchmark/tooling utilities, not part of the extension runtime.

**Core dependencies** (Microsoft-owned, all three are used across the codebase):

- `@microsoft/powerquery-parser` — Lexer, parser, and type validation
- `@microsoft/powerquery-language-services` — Higher-level language service (Analysis, completions, hover, etc.)
- `@microsoft/powerquery-formatter` — Code formatter (server-side only)

### Library Symbol System

External library symbols allow users to extend the M standard library with custom function definitions loaded from JSON files on disk. The flow:

1. User configures `powerquery.client.additionalSymbolsDirectories` setting
2. `LibrarySymbolManager` scans directories for `.json` files, parses them via `LibrarySymbolUtils`
3. `LibrarySymbolClient` sends symbols to the server via custom LSP requests (`powerquery/addLibrarySymbols`, `powerquery/removeLibrarySymbols`)
4. Server merges external symbols with built-in standard/SDK library in `SettingsUtils.getLibrary()`

### Local Development with Sibling Packages

Use `npm run link:start` to develop against locally-built copies of the parser, formatter, and language-services packages (via `npm link`). Use `npm run link:stop` to revert to published npm versions.

## Code Conventions

**TypeScript strictness** — The ESLint config enforces rules that are stricter than typical TypeScript projects:

- `explicit-function-return-type`: All functions must have explicit return type annotations
- `typedef`: Required on variables, parameters, properties, arrow parameters, and destructuring
- `no-floating-promises`: All promises must be awaited or handled
- `switch-exhaustiveness-check`: Switch statements must cover all cases
- `sort-imports`: Imports must be sorted (separated groups allowed, case-insensitive)
- `no-plusplus`: Use `+= 1` instead of `++`
- `object-shorthand`: Always use shorthand properties/methods
- `arrow-body-style`: Use concise arrow function bodies (no braces for single expressions)
- `curly`: Always use braces for control flow, even single-line

**Formatting** (Prettier): 120 char line width, 4-space indent, trailing commas, no parens on single arrow params.

**Import aliases**: The codebase uses `PQP` for powerquery-parser, `PQLS` for powerquery-language-services, and `PQF` for powerquery-formatter.

**Testing**: Server tests use Mocha (`describe`/`it`) with Chai `expect` and Node `assert`. Client tests use VS Code's test runner (`suite`/`test` TDD-style).
16 changes: 15 additions & 1 deletion client/.vscode-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { defineConfig } = require('@vscode/test-cli');
module.exports = defineConfig([
{
label: "UI Tests",
files: "lib/test/**/*.test.js",
files: "lib/test/**/!(multiRootWorkspace).test.js",
workspaceFolder: "src/test/testFixture",
extensionDevelopmentPath: "..",
launchArgs: ["--profile-temp", "--disable-extensions"],
Expand All @@ -26,5 +26,19 @@ module.exports = defineConfig([
// },
// }
}
},
{
label: "Multi-root Workspace Tests",
files: "lib/test/**/multiRootWorkspace.test.js",
workspaceFolder: "src/test/multiRootFixture/test.code-workspace",
extensionDevelopmentPath: "..",
launchArgs: ["--profile-temp", "--disable-extensions"],

mocha: {
color: true,
ui: "tdd",
timeout: 20000,
slow: 10000,
}
}
]);
48 changes: 24 additions & 24 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 41 additions & 11 deletions client/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<PowerQ
librarySymbolClient = new LibrarySymbolClient(client);
librarySymbolManager = new LibrarySymbolManager(librarySymbolClient, client);

await configureSymbolDirectories();
await configureAllFolderSymbolDirectories();

context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(async (event: vscode.ConfigurationChangeEvent) => {
Expand All @@ -73,9 +73,20 @@ export async function activate(context: vscode.ExtensionContext): Promise<PowerQ
);

if (event.affectsConfiguration(symbolDirs)) {
await configureSymbolDirectories();
await configureAllFolderSymbolDirectories();
}
}),
vscode.workspace.onDidChangeWorkspaceFolders(async (event: vscode.WorkspaceFoldersChangeEvent) => {
await Promise.all(
event.removed.map((folder: vscode.WorkspaceFolder) =>
librarySymbolManager.removeSymbolsForFolder(folder.uri.toString()),
),
);

await Promise.all(
event.added.map((folder: vscode.WorkspaceFolder) => configureSymbolDirectoriesForFolder(folder)),
);
}),
);

return Object.freeze(librarySymbolClient);
Expand All @@ -85,18 +96,37 @@ export function deactivate(): Thenable<void> | undefined {
return client?.stop();
}

async function configureSymbolDirectories(): Promise<void> {
const config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(ConfigurationConstant.BasePath);
async function configureAllFolderSymbolDirectories(): Promise<void> {
const folders: readonly vscode.WorkspaceFolder[] | undefined = vscode.workspace.workspaceFolders;

if (!folders || folders.length === 0) {
// No workspace folders — read global config
const config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(ConfigurationConstant.BasePath);

const additionalSymbolsDirectories: string[] | undefined = config.get(
ConfigurationConstant.AdditionalSymbolsDirectories,
);

await librarySymbolManager.refreshSymbolDirectories(additionalSymbolsDirectories ?? []);

return;
}

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configureAllFolderSymbolDirectories can register symbols under the global folder key when no workspace folders are present, but those global registrations are never cleared if/when workspace folders are later added. That can leave stale global modules registered on the server alongside per-folder modules (and can cause false “overlap” warnings / duplicated symbols). Consider clearing the global registrations when workspaceFolders becomes non-empty (e.g., call librarySymbolManager.removeSymbolsForFolder(GlobalFolderKey) before registering per-folder symbols, or call removeAllSymbols() and re-register).

Suggested change
// Clear any previously-registered global or stale folder symbols before
// rebuilding the current workspace-folder registrations.
await librarySymbolManager.removeAllSymbols();

Copilot uses AI. Check for mistakes.
await Promise.all(folders.map((folder: vscode.WorkspaceFolder) => configureSymbolDirectoriesForFolder(folder)));
}

async function configureSymbolDirectoriesForFolder(folder: vscode.WorkspaceFolder): Promise<void> {
const config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(
ConfigurationConstant.BasePath,
folder.uri,
);

const additionalSymbolsDirectories: string[] | undefined = config.get(
ConfigurationConstant.AdditionalSymbolsDirectories,
);

// TODO: Should we fix/remove invalid and malformed directory path values?
// For example, a quoted path "c:\path\to\file" will be considered invalid and reported as an error.
// We could modify values and write them back to the original config locations.

await librarySymbolManager.refreshSymbolDirectories(additionalSymbolsDirectories ?? []);

// TODO: Configure file system watchers to detect library file changes.
await librarySymbolManager.refreshSymbolDirectoriesForFolder(
folder.uri.toString(),
additionalSymbolsDirectories ?? [],
);
}
Loading
Loading