Skip to content

Commit 272316f

Browse files
committed
Add docker compose as a version manager
1 parent 30f8547 commit 272316f

20 files changed

+1058
-126
lines changed

vscode/package.json

+9
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@
328328
"rvm",
329329
"shadowenv",
330330
"mise",
331+
"compose",
331332
"custom"
332333
],
333334
"default": "auto"
@@ -347,6 +348,14 @@
347348
"chrubyRubies": {
348349
"description": "An array of extra directories to search for Ruby installations when using chruby. Equivalent to the RUBIES environment variable",
349350
"type": "array"
351+
},
352+
"composeService": {
353+
"description": "The name of the service in the compose file to use to start the Ruby LSP server",
354+
"type": "string"
355+
},
356+
"composeCustomCommand": {
357+
"description": "A shell command to start the ruby LSP server using compose. This overrides the composeService setting",
358+
"type": "string"
350359
}
351360
},
352361
"default": {

vscode/src/client.ts

+157-8
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;
@@ -74,8 +75,8 @@ function getLspExecutables(
7475
const executableOptions: ExecutableOptions = {
7576
cwd: workspaceFolder.uri.fsPath,
7677
env: bypassTypechecker
77-
? { ...env, RUBY_LSP_BYPASS_TYPECHECKER: "true" }
78-
: env,
78+
? { ...ruby.env, RUBY_LSP_BYPASS_TYPECHECKER: "true" }
79+
: ruby.env,
7980
shell: true,
8081
};
8182

@@ -129,6 +130,9 @@ function getLspExecutables(
129130
};
130131
}
131132

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

@@ -166,6 +170,32 @@ function collectClientOptions(
166170
},
167171
);
168172

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

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

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

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

321409
#context: vscode.ExtensionContext;
322410
#formatter: string;
@@ -334,7 +422,7 @@ export default class Client extends LanguageClient implements ClientInterface {
334422
) {
335423
super(
336424
LSP_NAME,
337-
getLspExecutables(workspaceFolder, ruby.env),
425+
getLspExecutables(workspaceFolder, ruby),
338426
collectClientOptions(
339427
vscode.workspace.getConfiguration("rubyLsp"),
340428
workspaceFolder,
@@ -349,6 +437,7 @@ export default class Client extends LanguageClient implements ClientInterface {
349437
this.registerFeature(new ExperimentalCapabilities());
350438
this.workspaceOutputChannel = outputChannel;
351439
this.virtualDocuments = virtualDocuments;
440+
this.pathConverter = ruby.pathConverter;
352441

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

vscode/src/common.ts

+10-1
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

@@ -63,11 +63,20 @@ export interface WorkspaceInterface {
6363
error: boolean;
6464
}
6565

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

78+
export const spawn = originalSpawn;
79+
7180
export const asyncExec = promisify(exec);
7281
export const LSP_NAME = "Ruby LSP";
7382
export const LOG_CHANNEL = vscode.window.createOutputChannel(LSP_NAME, {

0 commit comments

Comments
 (0)