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

feat(lsp-cli): make it better and more docs #3

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
11 changes: 1 addition & 10 deletions README.md
@@ -1,12 +1,3 @@
# @izaakschroeder/lsp-tools

Tools to work with your LSP.

```sh
lsp \
--connect 'stdio://biome/?arg=lsp-proxy' \
fix \
--ignore '**/node_modules/**' \
--rule 'quickfix.suppressRule.biome.*' \
'./src/**/*{.ts,.tsx,.js,.jsx,.mjs,.cjs}'
```
Tools and libraries to fulfill your LSP needs.
8 changes: 4 additions & 4 deletions packages/lsp-cli/README.md
Expand Up @@ -7,8 +7,8 @@
Use the LSP `textDocument/codeAction` to perform specific actions on files.

```sh
lsp fix --connect 'stdio://biome/?arg=lsp-proxy' \
lsp fix --connect "stdio://$(yarn bin biome)/?arg=lsp-proxy" \
--ignore '**/node_modules/**' \
--action-kind 'quickfix.suppressRule.biome.*' \
'./src/**/*{.ts,.tsx,.js,.jsx,.mjs,.cjs}'
```
--action-kind 'quickfix.suppressRule.biome.**' \
'**/*{.ts,.tsx,.js,.jsx,.mjs,.cjs}'
```
21 changes: 10 additions & 11 deletions packages/lsp-cli/package.json
Expand Up @@ -2,33 +2,32 @@
"name": "@izaakschroeder/lsp-cli",
"version": "0.0.1",
"type": "module",
"main": "./src/index.ts",
"bin": {
"lsp": "./dist/cli.js"
},
"files": ["./dist/**", "README.md", "package.json"],
"scripts": {
"build": "rollup -c",
"test": "biome check ./"
},
"dependencies": {
"@izaakschroeder/lsp-client": "workspace:^",
"chalk": "5.3.0",
"cli-progress": "3.12.0",
"clipanion": "4.0.0-rc.3",
"ku-progress-bar": "0.6.0",
"picomatch": "4.0.1",
"throat": "6.0.2",
"typanion": "3.14.0"
},
"devDependencies": {
"@biomejs/biome": "1.6.1",
"@izaakschroeder/lsp-client": "workspace:^",
"@rollup/plugin-commonjs": "25.0.7",
"@rollup/plugin-node-resolve": "15.2.3",
"@types/node": "20.11.29",
"@types/picomatch": "2.3.3",
"chalk": "5.3.0",
"cli-progress": "3.12.0",
"clipanion": "4.0.0-rc.3",
"esbuild": "0.20.2",
"fast-deep-equal": "3.1.3",
"ku-progress-bar": "0.6.0",
"picomatch": "4.0.1",
"rollup": "4.13.0",
"rollup-plugin-esbuild": "6.1.1",
"throat": "6.0.2",
"typanion": "3.14.0",
"typescript": "5.4.2"
}
}
4 changes: 3 additions & 1 deletion packages/lsp-cli/rollup.config.js
Expand Up @@ -10,7 +10,9 @@ export default [
exportConditions: ['node'],
}),
commonjs(),
esbuild(),
esbuild({
target: 'es2022',
}),
],
output: [
{
Expand Down
27 changes: 21 additions & 6 deletions packages/lsp-cli/src/FixCommand.ts
Expand Up @@ -10,6 +10,7 @@ import * as t from 'typanion';

import { BaseLspCommand } from './BaseLspCommand';
import { createActionFilter } from './createActionFilter';
import { createActionRewrite } from './createActionRewrite';
import { fixFile } from './fixFile';
import { glob } from './glob';

Expand Down Expand Up @@ -49,7 +50,19 @@ export class FixCommand extends BaseLspCommand {
Can be specified more than once to include multiple patterns.
`,
});
actionTransform = Option.String('--action-transform');
actionTextReplace = Option.Array('--action-text-replace', {
description: `
Pass a \`needle=haystack\` search/replace string to modify the
result after an action has been performed but before it has been
applied to your files.
`,
});
actionIgnoreDuplicates = Option.Boolean('--action-ignore-duplicates', {
description: `
If multiple actions are presented that do the same thing, ignore
all but one of them.
`,
});

// TODO(@izaakschroeder): Move this to `BaseLspCommand`
// TODO(@izaakschroeder): Do feature detection for workspaces.
Expand Down Expand Up @@ -115,12 +128,14 @@ export class FixCommand extends BaseLspCommand {

const actionFilter = createActionFilter(this.actionKinds);
let actionMap = null;
if (this.actionTransform) {
const parentURL = pathToFileURL(process.cwd());
const resolved = import.meta.resolve(this.actionTransform, parentURL);
actionMap = await import(resolved);
if (this.actionTextReplace) {
actionMap = createActionRewrite(this.actionTextReplace);
}
const fixOptions = { actionFilter, actionMap };
const fixOptions = {
actionFilter,
actionMap,
ignoreDuplicateActions: this.actionIgnoreDuplicates,
};

const exec = createThroat(this.parallel, async (path) => {
return await fixFile(lsp, path, fixOptions);
Expand Down
27 changes: 27 additions & 0 deletions packages/lsp-cli/src/createActionRewrite.ts
@@ -0,0 +1,27 @@
import type { CodeActionItem } from '@izaakschroeder/lsp-client';

export const createActionRewrite = (patterns: string[]) => {
const rewrites = patterns.map((patterns) => {
const [find, replace] = patterns.split('=', 2);
if (!find || !replace) {
throw new Error();
}
return (action: CodeActionItem) => {
for (const key in action.edit.changes) {
const changes = action.edit.changes[key];
if (!changes) {
continue;
}
for (const change of changes) {
change.newText = change.newText.replaceAll(find, replace);
}
}
return action;
};
});
return (action: CodeActionItem) => {
return rewrites.reduce((prev, cur) => {
return cur(prev);
}, action);
};
};
12 changes: 11 additions & 1 deletion packages/lsp-cli/src/fixFile.ts
@@ -1,6 +1,7 @@
import * as fs from 'node:fs/promises';
import { extname } from 'node:path';
import { pathToFileURL } from 'node:url';
import isEqual from 'fast-deep-equal/es6';

import type {
CodeActionItem,
Expand All @@ -24,6 +25,7 @@ const languageMap = {
interface FixFileOptions {
actionFilter?: ((action: CodeActionItem) => boolean) | null | undefined;
actionMap?: ((action: CodeActionItem) => CodeActionItem) | null | undefined;
ignoreDuplicateActions?: boolean | null | undefined;
}

export const fixFile = async (
Expand Down Expand Up @@ -80,8 +82,16 @@ export const fixFile = async (
if (actions.length === 0) {
return false;
}
let desiredActions = actions.filter((action) => {
let desiredActions = actions.filter((action, i) => {
let ignoredBecauseDuplicate = false;
if (options.ignoreDuplicateActions) {
const otherIndex = actions.findIndex((other) => {
return isEqual(other, action);
});
ignoredBecauseDuplicate = otherIndex !== i;
}
return (
!ignoredBecauseDuplicate &&
(options.actionFilter ? options.actionFilter(action) : true) &&
action.edit.changes[uri] &&
Object.keys(action.edit.changes).length === 1
Expand Down
6 changes: 3 additions & 3 deletions packages/lsp-cli/src/glob.ts
Expand Up @@ -13,15 +13,15 @@ export const glob = async (
cb: (path: string) => Promise<void>,
) => {
const { ignore, include } = options;
const isIgnored = ignore ? picomatch(ignore) : () => false;
const isIncluded = include ? picomatch(include) : () => true;
const isIgnored = ignore ? picomatch(ignore, { dot: true }) : () => false;
const isIncluded = include ? picomatch(include, { dot: true }) : () => true;

const processDir = async (root: string, dir: string) => {
const entries = await fs.readdir(dir, { withFileTypes: true });
await Promise.all(
entries.map(async (entry) => {
const path = join(dir, entry.name);
const relativePath = path.substring(root.length);
const relativePath = path.substring(root.length + 1);
if (isIgnored(relativePath)) {
return;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/lsp-client/src/LspClient.ts
Expand Up @@ -71,6 +71,9 @@ export class LspClient {
}

#send(req: JSONRPCPayload) {
if (!this.#connections.length) {
throw new Error('Not connected.');
}
for (const connection of this.#connections) {
connection.send(req);
}
Expand Down
7 changes: 6 additions & 1 deletion packages/lsp-client/src/createTransport.ts
@@ -1,10 +1,15 @@
import { fileURLToPath } from 'node:url';
import { LspStdioTransport } from './LspStdioTransport';

export const createTransport = (urlString: string) => {
const url = new URL(urlString);
switch (url.protocol) {
case 'stdio:': {
const bin = url.hostname;
if (url.hostname) {
// TODO: Consider resolving non-absolute paths
throw new TypeError('Path must be absolute');
}
const bin = fileURLToPath(`file://${url.pathname}`);
const args = url.searchParams.getAll('arg');
return new LspStdioTransport(bin, args);
}
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Expand Up @@ -285,6 +285,7 @@ __metadata:
cli-progress: "npm:3.12.0"
clipanion: "npm:4.0.0-rc.3"
esbuild: "npm:0.20.2"
fast-deep-equal: "npm:3.1.3"
ku-progress-bar: "npm:0.6.0"
picomatch: "npm:4.0.1"
rollup: "npm:4.13.0"
Expand Down Expand Up @@ -880,6 +881,13 @@ __metadata:
languageName: node
linkType: hard

"fast-deep-equal@npm:3.1.3":
version: 3.1.3
resolution: "fast-deep-equal@npm:3.1.3"
checksum: 10c0/40dedc862eb8992c54579c66d914635afbec43350afbbe991235fdcb4e3a8d5af1b23ae7e79bef7d4882d0ecee06c3197488026998fb19f72dc95acff1d1b1d0
languageName: node
linkType: hard

"foreground-child@npm:^3.1.0":
version: 3.1.1
resolution: "foreground-child@npm:3.1.1"
Expand Down