Skip to content

Commit 69c6d3a

Browse files
authored
Merge pull request #160 from atom-community/configuring-project-path
2 parents 813fbde + 61fec58 commit 69c6d3a

File tree

3 files changed

+145
-24
lines changed

3 files changed

+145
-24
lines changed

lib/auto-languageclient.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@ import * as Utils from "./utils"
2626
import { Socket } from "net"
2727
import { LanguageClientConnection } from "./languageclient"
2828
import { ConsoleLogger, FilteredLogger, Logger } from "./logger"
29-
import { LanguageServerProcess, ServerManager, ActiveServer } from "./server-manager.js"
29+
import {
30+
LanguageServerProcess,
31+
ServerManager,
32+
ActiveServer,
33+
normalizePath,
34+
considerAdditionalPath,
35+
} from "./server-manager.js"
3036
import { Disposable, CompositeDisposable, Point, Range, TextEditor } from "atom"
3137
import * as ac from "atom/autocomplete-plus"
3238
import { basename } from "path"
@@ -308,7 +314,8 @@ export default class AutoLanguageClient {
308314
(e) => this.shouldStartForEditor(e),
309315
(filepath) => this.filterChangeWatchedFiles(filepath),
310316
this.reportBusyWhile,
311-
this.getServerName()
317+
this.getServerName(),
318+
this.determineProjectPath
312319
)
313320
this._serverManager.startListening()
314321
process.on("exit", () => this.exitCleanup.bind(this))
@@ -390,6 +397,7 @@ export default class AutoLanguageClient {
390397
connection,
391398
capabilities: initializeResponse.capabilities,
392399
disposable: new CompositeDisposable(),
400+
additionalPaths: new Set<string>(),
393401
}
394402
this.postInitialization(newServer)
395403
connection.initialized()
@@ -477,6 +485,27 @@ export default class AutoLanguageClient {
477485
this.logger.debug(`exit: code ${code} signal ${signal}`)
478486
}
479487

488+
/** (Optional) Finds the project path. If there is a custom logic for finding projects override this method. */
489+
protected determineProjectPath(textEditor: TextEditor): string | null {
490+
const filePath = textEditor.getPath()
491+
// TODO can filePath be null
492+
if (filePath === null || filePath === undefined) {
493+
return null
494+
}
495+
const projectPath = this._serverManager.getNormalizedProjectPaths().find((d) => filePath.startsWith(d))
496+
if (projectPath !== undefined) {
497+
return projectPath
498+
}
499+
500+
const serverWithClaim = this._serverManager
501+
.getActiveServers()
502+
.find((server) => server.additionalPaths?.has(path.dirname(filePath)))
503+
if (serverWithClaim !== undefined) {
504+
return normalizePath(serverWithClaim.projectPath)
505+
}
506+
return null
507+
}
508+
480509
/**
481510
* The function called whenever the spawned server returns `data` in `stderr` Extend (call super.onSpawnStdErrData) or
482511
* override this if you need custom stderr data handling
@@ -668,7 +697,23 @@ export default class AutoLanguageClient {
668697
}
669698

670699
this.definitions = this.definitions || new DefinitionAdapter()
671-
return this.definitions.getDefinition(server.connection, server.capabilities, this.getLanguageName(), editor, point)
700+
const query = await this.definitions.getDefinition(
701+
server.connection,
702+
server.capabilities,
703+
this.getLanguageName(),
704+
editor,
705+
point
706+
)
707+
708+
if (query !== null && server.additionalPaths !== undefined) {
709+
// populate additionalPaths based on definitions
710+
// Indicates that the language server can support LSP functionality for out of project files indicated by `textDocument/definition` responses.
711+
for (const def of query.definitions) {
712+
considerAdditionalPath(server as ActiveServer & { additionalPaths: Set<string> }, path.dirname(def.path))
713+
}
714+
}
715+
716+
return query
672717
}
673718

674719
// Outline View via LS documentSymbol---------------------------------

lib/server-manager.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export interface ActiveServer {
2222
process: LanguageServerProcess
2323
connection: ls.LanguageClientConnection
2424
capabilities: ls.ServerCapabilities
25+
/** Out of project directories that this server can also support. */
26+
additionalPaths?: Set<string>
2527
}
2628

2729
interface RestartCounter {
@@ -47,7 +49,8 @@ export class ServerManager {
4749
private _startForEditor: (editor: TextEditor) => boolean,
4850
private _changeWatchedFileFilter: (filePath: string) => boolean,
4951
private _reportBusyWhile: ReportBusyWhile,
50-
private _languageServerName: string
52+
private _languageServerName: string,
53+
private _determineProjectPath: (textEditor: TextEditor) => string | null
5154
) {
5255
this.updateNormalizedProjectPaths()
5356
}
@@ -121,7 +124,7 @@ export class ServerManager {
121124
textEditor: TextEditor,
122125
{ shouldStart }: { shouldStart?: boolean } = { shouldStart: false }
123126
): Promise<ActiveServer | null> {
124-
const finalProjectPath = this.determineProjectPath(textEditor)
127+
const finalProjectPath = this._determineProjectPath(textEditor)
125128
if (finalProjectPath == null) {
126129
// Files not yet saved have no path
127130
return null
@@ -245,14 +248,6 @@ export class ServerManager {
245248
})
246249
}
247250

248-
public determineProjectPath(textEditor: TextEditor): string | null {
249-
const filePath = textEditor.getPath()
250-
if (filePath == null) {
251-
return null
252-
}
253-
return this._normalizedProjectPaths.find((d) => filePath.startsWith(d)) || null
254-
}
255-
256251
public updateNormalizedProjectPaths(): void {
257252
this._normalizedProjectPaths = atom.project.getPaths().map(normalizePath)
258253
}
@@ -350,3 +345,13 @@ export function normalizedProjectPathToWorkspaceFolder(normalizedProjectPath: st
350345
export function normalizePath(projectPath: string): string {
351346
return !projectPath.endsWith(path.sep) ? path.join(projectPath, path.sep) : projectPath
352347
}
348+
349+
/** Considers a path for inclusion in `additionalPaths`. */
350+
export function considerAdditionalPath(
351+
server: ActiveServer & { additionalPaths: Set<string> },
352+
additionalPath: string
353+
): void {
354+
if (!additionalPath.startsWith(server.projectPath)) {
355+
server.additionalPaths.add(additionalPath)
356+
}
357+
}

test/auto-languageclient.test.ts

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
import { TextEditor } from "atom"
12
import AutoLanguageClient from "../lib/auto-languageclient"
2-
import { projectPathToWorkspaceFolder, ServerManager } from "../lib/server-manager"
3+
import {
4+
projectPathToWorkspaceFolder,
5+
ServerManager,
6+
ActiveServer,
7+
considerAdditionalPath,
8+
normalizePath,
9+
} from "../lib/server-manager"
310
import { FakeAutoLanguageClient } from "./helpers"
4-
import { dirname } from "path"
11+
import { dirname, join } from "path"
512

613
function mockEditor(uri: string, scopeName: string): any {
714
return {
@@ -12,20 +19,84 @@ function mockEditor(uri: string, scopeName: string): any {
1219
}
1320
}
1421

22+
function setupClient() {
23+
atom.workspace.getTextEditors().forEach((editor) => editor.destroy())
24+
atom.project.getPaths().forEach((project) => atom.project.removePath(project))
25+
const client = new FakeAutoLanguageClient()
26+
client.activate()
27+
return client
28+
}
29+
30+
function setupServerManager(client = setupClient()) {
31+
/* eslint-disable-next-line dot-notation */
32+
const serverManager = client["_serverManager"]
33+
return serverManager
34+
}
35+
1536
describe("AutoLanguageClient", () => {
37+
describe("determineProjectPath", () => {
38+
it("returns the project path for an internal or an external file in the project", async () => {
39+
if (process.platform === "darwin") {
40+
// there is nothing OS specific about the code. It just hits the limits that MacOS can handle in this test
41+
pending("skipped on MacOS")
42+
return
43+
}
44+
const client = setupClient()
45+
const serverManager = setupServerManager(client)
46+
47+
// "returns null when a single file is open"
48+
49+
let textEditor = (await atom.workspace.open(__filename)) as TextEditor
50+
/* eslint-disable-next-line dot-notation */
51+
expect(client["determineProjectPath"](textEditor)).toBeNull()
52+
textEditor.destroy()
53+
54+
// "returns the project path when a file of that project is open"
55+
const projectPath = __dirname
56+
57+
// gives the open workspace folder
58+
atom.project.addPath(projectPath)
59+
await serverManager.startServer(projectPath)
60+
61+
textEditor = (await atom.workspace.open(__filename)) as TextEditor
62+
/* eslint-disable-next-line dot-notation */
63+
expect(client["determineProjectPath"](textEditor)).toBe(normalizePath(projectPath))
64+
textEditor.destroy()
65+
66+
// "returns the project path when an external file is open and it is not in additional paths"
67+
68+
const externalDir = join(dirname(projectPath), "lib")
69+
const externalFile = join(externalDir, "main.js")
70+
71+
// gives the open workspace folder
72+
atom.project.addPath(projectPath)
73+
await serverManager.startServer(projectPath)
74+
75+
textEditor = (await atom.workspace.open(externalFile)) as TextEditor
76+
/* eslint-disable-next-line dot-notation */
77+
expect(client["determineProjectPath"](textEditor)).toBeNull()
78+
textEditor.destroy()
79+
80+
// "returns the project path when an external file is open and it is in additional paths"
81+
82+
// get server
83+
const server = serverManager.getActiveServers()[0]
84+
expect(typeof server.additionalPaths).toBe("object") // Set()
85+
// add additional path
86+
considerAdditionalPath(server as ActiveServer & { additionalPaths: Set<string> }, externalDir)
87+
expect(server.additionalPaths?.has(externalDir)).toBeTrue()
88+
89+
textEditor = (await atom.workspace.open(externalFile)) as TextEditor
90+
/* eslint-disable-next-line dot-notation */
91+
expect(client["determineProjectPath"](textEditor)).toBe(normalizePath(projectPath))
92+
textEditor.destroy()
93+
})
94+
})
1695
describe("ServerManager", () => {
1796
describe("WorkspaceFolders", () => {
18-
let client: FakeAutoLanguageClient
1997
let serverManager: ServerManager
20-
2198
beforeEach(() => {
22-
atom.workspace.getTextEditors().forEach((editor) => editor.destroy())
23-
atom.project.getPaths().forEach((project) => atom.project.removePath(project))
24-
client = new FakeAutoLanguageClient()
25-
client.activate()
26-
27-
/* eslint-disable-next-line dot-notation */
28-
serverManager = client["_serverManager"]
99+
serverManager = setupServerManager()
29100
})
30101

31102
afterEach(() => {

0 commit comments

Comments
 (0)