Skip to content

Commit 2844728

Browse files
committed
Add docker compose as a version manager
1 parent 1c6fea9 commit 2844728

20 files changed

+1058
-126
lines changed

vscode/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@
338338
"rvm",
339339
"shadowenv",
340340
"mise",
341+
"compose",
341342
"custom"
342343
],
343344
"default": "auto"
@@ -357,6 +358,14 @@
357358
"chrubyRubies": {
358359
"description": "An array of extra directories to search for Ruby installations when using chruby. Equivalent to the RUBIES environment variable",
359360
"type": "array"
361+
},
362+
"composeService": {
363+
"description": "The name of the service in the compose file to use to start the Ruby LSP server",
364+
"type": "string"
365+
},
366+
"composeCustomCommand": {
367+
"description": "A shell command to start the ruby LSP server using compose",
368+
"type": "string"
360369
}
361370
},
362371
"default": {

vscode/src/client.ts

Lines changed: 157 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
SUPPORTED_LANGUAGE_IDS,
3838
FEATURE_FLAGS,
3939
featureEnabled,
40+
PathConverterInterface,
4041
} from "./common";
4142
import { Ruby } from "./ruby";
4243
import { WorkspaceChannel } from "./workspaceChannel";
@@ -60,7 +61,7 @@ function enabledFeatureFlags(): Record<string, boolean> {
6061
// Get the executables to start the server based on the user's configuration
6162
function getLspExecutables(
6263
workspaceFolder: vscode.WorkspaceFolder,
63-
env: NodeJS.ProcessEnv,
64+
ruby: Ruby,
6465
): ServerOptions {
6566
let run: Executable;
6667
let debug: Executable;
@@ -73,8 +74,8 @@ function getLspExecutables(
7374
const executableOptions: ExecutableOptions = {
7475
cwd: workspaceFolder.uri.fsPath,
7576
env: bypassTypechecker
76-
? { ...env, RUBY_LSP_BYPASS_TYPECHECKER: "true" }
77-
: env,
77+
? { ...ruby.env, RUBY_LSP_BYPASS_TYPECHECKER: "true" }
78+
: ruby.env,
7879
shell: true,
7980
};
8081

@@ -128,6 +129,9 @@ function getLspExecutables(
128129
};
129130
}
130131

132+
run = ruby.activateExecutable(run);
133+
debug = ruby.activateExecutable(debug);
134+
131135
return { run, debug };
132136
}
133137

@@ -165,6 +169,32 @@ function collectClientOptions(
165169
},
166170
);
167171

172+
const pathConverter = ruby.pathConverter;
173+
174+
const pushAlternativePaths = (
175+
path: string,
176+
schemes: string[] = supportedSchemes,
177+
) => {
178+
schemes.forEach((scheme) => {
179+
[
180+
pathConverter.toLocalPath(path),
181+
pathConverter.toRemotePath(path),
182+
].forEach((convertedPath) => {
183+
if (convertedPath !== path) {
184+
SUPPORTED_LANGUAGE_IDS.forEach((language) => {
185+
documentSelector.push({
186+
scheme,
187+
language,
188+
pattern: `${convertedPath}/**/*`,
189+
});
190+
});
191+
}
192+
});
193+
});
194+
};
195+
196+
pushAlternativePaths(fsPath);
197+
168198
// Only the first language server we spawn should handle unsaved files, otherwise requests will be duplicated across
169199
// all workspaces
170200
if (isMainWorkspace) {
@@ -184,6 +214,8 @@ function collectClientOptions(
184214
pattern: `${gemPath}/**/*`,
185215
});
186216

217+
pushAlternativePaths(gemPath, [scheme]);
218+
187219
// Because of how default gems are installed, the gemPath location is actually not exactly where the files are
188220
// located. With the regex, we are correcting the default gem path from this (where the files are not located)
189221
// /opt/rubies/3.3.1/lib/ruby/gems/3.3.0
@@ -194,15 +226,50 @@ function collectClientOptions(
194226
// Notice that we still need to add the regular path to the selector because some version managers will install
195227
// gems under the non-corrected path
196228
if (/lib\/ruby\/gems\/(?=\d)/.test(gemPath)) {
229+
const correctedPath = gemPath.replace(
230+
/lib\/ruby\/gems\/(?=\d)/,
231+
"lib/ruby/",
232+
);
233+
197234
documentSelector.push({
198235
scheme,
199236
language: "ruby",
200-
pattern: `${gemPath.replace(/lib\/ruby\/gems\/(?=\d)/, "lib/ruby/")}/**/*`,
237+
pattern: `${correctedPath}/**/*`,
201238
});
239+
240+
pushAlternativePaths(correctedPath, [scheme]);
202241
}
203242
});
204243
});
205244

245+
// Add other mapped paths to the document selector
246+
pathConverter.pathMapping.forEach(([local, remote]) => {
247+
if (
248+
(documentSelector as { pattern: string }[]).some(
249+
(selector) =>
250+
selector.pattern?.startsWith(local) ||
251+
selector.pattern?.startsWith(remote),
252+
)
253+
) {
254+
return;
255+
}
256+
257+
supportedSchemes.forEach((scheme) => {
258+
SUPPORTED_LANGUAGE_IDS.forEach((language) => {
259+
documentSelector.push({
260+
language,
261+
pattern: `${local}/**/*`,
262+
});
263+
264+
documentSelector.push({
265+
scheme,
266+
language,
267+
pattern: `${remote}/**/*`,
268+
});
269+
});
270+
});
271+
});
272+
206273
// This is a temporary solution as an escape hatch for users who cannot upgrade the `ruby-lsp` gem to a version that
207274
// supports ERB
208275
if (!configuration.get<boolean>("erbSupport")) {
@@ -211,9 +278,29 @@ function collectClientOptions(
211278
});
212279
}
213280

281+
outputChannel.info(
282+
`Document Selector Paths: ${JSON.stringify(documentSelector)}`,
283+
);
284+
285+
// Map using pathMapping
286+
const code2Protocol = (uri: vscode.Uri) => {
287+
const remotePath = pathConverter.toRemotePath(uri.fsPath);
288+
return vscode.Uri.file(remotePath).toString();
289+
};
290+
291+
const protocol2Code = (uri: string) => {
292+
const remoteUri = vscode.Uri.parse(uri);
293+
const localPath = pathConverter.toLocalPath(remoteUri.fsPath);
294+
return vscode.Uri.file(localPath);
295+
};
296+
214297
return {
215298
documentSelector,
216299
workspaceFolder,
300+
uriConverters: {
301+
code2Protocol,
302+
protocol2Code,
303+
},
217304
diagnosticCollectionName: LSP_NAME,
218305
outputChannel,
219306
revealOutputChannelOn: RevealOutputChannelOn.Never,
@@ -316,6 +403,7 @@ export default class Client extends LanguageClient implements ClientInterface {
316403
private readonly baseFolder;
317404
private readonly workspaceOutputChannel: WorkspaceChannel;
318405
private readonly virtualDocuments = new Map<string, string>();
406+
private readonly pathConverter: PathConverterInterface;
319407

320408
#context: vscode.ExtensionContext;
321409
#formatter: string;
@@ -333,7 +421,7 @@ export default class Client extends LanguageClient implements ClientInterface {
333421
) {
334422
super(
335423
LSP_NAME,
336-
getLspExecutables(workspaceFolder, ruby.env),
424+
getLspExecutables(workspaceFolder, ruby),
337425
collectClientOptions(
338426
vscode.workspace.getConfiguration("rubyLsp"),
339427
workspaceFolder,
@@ -348,6 +436,7 @@ export default class Client extends LanguageClient implements ClientInterface {
348436
this.registerFeature(new ExperimentalCapabilities());
349437
this.workspaceOutputChannel = outputChannel;
350438
this.virtualDocuments = virtualDocuments;
439+
this.pathConverter = ruby.pathConverter;
351440

352441
// Middleware are part of client options, but because they must reference `this`, we cannot make it a part of the
353442
// `super` call (TypeScript does not allow accessing `this` before invoking `super`)
@@ -428,7 +517,9 @@ export default class Client extends LanguageClient implements ClientInterface {
428517
range?: Range,
429518
): Promise<{ ast: string } | null> {
430519
return this.sendRequest("rubyLsp/textDocument/showSyntaxTree", {
431-
textDocument: { uri: uri.toString() },
520+
textDocument: {
521+
uri: this.pathConverter.toRemoteUri(uri).toString(),
522+
},
432523
range,
433524
});
434525
}
@@ -624,10 +715,12 @@ export default class Client extends LanguageClient implements ClientInterface {
624715
token,
625716
_next,
626717
) => {
718+
const remoteUri = this.pathConverter.toRemoteUri(document.uri);
719+
627720
const response: vscode.TextEdit[] | null = await this.sendRequest(
628721
"textDocument/onTypeFormatting",
629722
{
630-
textDocument: { uri: document.uri.toString() },
723+
textDocument: { uri: remoteUri.toString() },
631724
position,
632725
ch,
633726
options,
@@ -695,9 +788,65 @@ export default class Client extends LanguageClient implements ClientInterface {
695788
token?: vscode.CancellationToken,
696789
) => Promise<T>,
697790
) => {
698-
return this.benchmarkMiddleware(type, param, () =>
791+
this.workspaceOutputChannel.trace(
792+
`Sending request: ${JSON.stringify(type)} with params: ${JSON.stringify(param)}`,
793+
);
794+
795+
const result = (await this.benchmarkMiddleware(type, param, () =>
699796
next(type, param, token),
797+
)) as any;
798+
799+
this.workspaceOutputChannel.trace(
800+
`Received response for ${JSON.stringify(type)}: ${JSON.stringify(result)}`,
700801
);
802+
803+
const request = typeof type === "string" ? type : type.method;
804+
805+
try {
806+
switch (request) {
807+
case "rubyLsp/workspace/dependencies":
808+
return result.map((dep: { path: string }) => {
809+
return {
810+
...dep,
811+
path: this.pathConverter.toLocalPath(dep.path),
812+
};
813+
});
814+
815+
case "textDocument/codeAction":
816+
return result.map((action: { uri: string }) => {
817+
const remotePath = vscode.Uri.parse(action.uri).fsPath;
818+
const localPath = this.pathConverter.toLocalPath(remotePath);
819+
820+
return {
821+
...action,
822+
uri: vscode.Uri.file(localPath).toString(),
823+
};
824+
});
825+
826+
case "textDocument/hover":
827+
if (
828+
result?.contents?.kind === "markdown" &&
829+
result.contents.value
830+
) {
831+
result.contents.value = result.contents.value.replace(
832+
/\((file:\/\/.+?)#/gim,
833+
(_match: string, path: string) => {
834+
const remotePath = vscode.Uri.parse(path).fsPath;
835+
const localPath =
836+
this.pathConverter.toLocalPath(remotePath);
837+
return `(${vscode.Uri.file(localPath).toString()}#`;
838+
},
839+
);
840+
}
841+
break;
842+
}
843+
} catch (error) {
844+
this.workspaceOutputChannel.error(
845+
`Error while processing response for ${request}: ${error}`,
846+
);
847+
}
848+
849+
return result;
701850
},
702851
sendNotification: async <TR>(
703852
type: string | MessageSignature,

vscode/src/common.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { exec } from "child_process";
1+
import { exec, spawn as originalSpawn } from "child_process";
22
import { createHash } from "crypto";
33
import { promisify } from "util";
44

@@ -65,11 +65,20 @@ export interface WorkspaceInterface {
6565
error: boolean;
6666
}
6767

68+
export interface PathConverterInterface {
69+
pathMapping: [string, string][];
70+
toRemotePath: (localPath: string) => string;
71+
toLocalPath: (remotePath: string) => string;
72+
toRemoteUri: (localUri: vscode.Uri) => vscode.Uri;
73+
}
74+
6875
// Event emitter used to signal that the language status items need to be refreshed
6976
export const STATUS_EMITTER = new vscode.EventEmitter<
7077
WorkspaceInterface | undefined
7178
>();
7279

80+
export const spawn = originalSpawn;
81+
7382
export const asyncExec = promisify(exec);
7483
export const LSP_NAME = "Ruby LSP";
7584
export const LOG_CHANNEL = vscode.window.createOutputChannel(LSP_NAME, {

0 commit comments

Comments
 (0)