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: support out of the project files #160

Merged
merged 10 commits into from
Jun 13, 2021
51 changes: 48 additions & 3 deletions lib/auto-languageclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ import * as Utils from "./utils"
import { Socket } from "net"
import { LanguageClientConnection } from "./languageclient"
import { ConsoleLogger, FilteredLogger, Logger } from "./logger"
import { LanguageServerProcess, ServerManager, ActiveServer } from "./server-manager.js"
import {
LanguageServerProcess,
ServerManager,
ActiveServer,
normalizePath,
considerAdditionalPath,
} from "./server-manager.js"
import { Disposable, CompositeDisposable, Point, Range, TextEditor } from "atom"
import * as ac from "atom/autocomplete-plus"
import { basename } from "path"
Expand Down Expand Up @@ -308,7 +314,8 @@ export default class AutoLanguageClient {
(e) => this.shouldStartForEditor(e),
(filepath) => this.filterChangeWatchedFiles(filepath),
this.reportBusyWhile,
this.getServerName()
this.getServerName(),
this.determineProjectPath
)
this._serverManager.startListening()
process.on("exit", () => this.exitCleanup.bind(this))
Expand Down Expand Up @@ -390,6 +397,7 @@ export default class AutoLanguageClient {
connection,
capabilities: initializeResponse.capabilities,
disposable: new CompositeDisposable(),
additionalPaths: new Set<string>(),
}
this.postInitialization(newServer)
connection.initialized()
Expand Down Expand Up @@ -477,6 +485,27 @@ export default class AutoLanguageClient {
this.logger.debug(`exit: code ${code} signal ${signal}`)
}

/** (Optional) Finds the project path. If there is a custom logic for finding projects override this method. */
protected determineProjectPath(textEditor: TextEditor): string | null {
const filePath = textEditor.getPath()
// TODO can filePath be null
if (filePath === null || filePath === undefined) {
return null
}
const projectPath = this._serverManager.getNormalizedProjectPaths().find((d) => filePath.startsWith(d))
if (projectPath !== undefined) {
return projectPath
}

const serverWithClaim = this._serverManager
.getActiveServers()
.find((server) => server.additionalPaths?.has(path.dirname(filePath)))
if (serverWithClaim !== undefined) {
return normalizePath(serverWithClaim.projectPath)
}
return null
}

/**
* The function called whenever the spawned server returns `data` in `stderr` Extend (call super.onSpawnStdErrData) or
* override this if you need custom stderr data handling
Expand Down Expand Up @@ -668,7 +697,23 @@ export default class AutoLanguageClient {
}

this.definitions = this.definitions || new DefinitionAdapter()
return this.definitions.getDefinition(server.connection, server.capabilities, this.getLanguageName(), editor, point)
const query = await this.definitions.getDefinition(
server.connection,
server.capabilities,
this.getLanguageName(),
editor,
point
)

if (query !== null && server.additionalPaths !== undefined) {
// populate additionalPaths based on definitions
// Indicates that the language server can support LSP functionality for out of project files indicated by `textDocument/definition` responses.
for (const def of query.definitions) {
considerAdditionalPath(server as ActiveServer & { additionalPaths: Set<string> }, path.dirname(def.path))
}
}

return query
}

// Outline View via LS documentSymbol---------------------------------
Expand Down
25 changes: 15 additions & 10 deletions lib/server-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export interface ActiveServer {
process: LanguageServerProcess
connection: ls.LanguageClientConnection
capabilities: ls.ServerCapabilities
/** Out of project directories that this server can also support. */
additionalPaths?: Set<string>
}

interface RestartCounter {
Expand All @@ -47,7 +49,8 @@ export class ServerManager {
private _startForEditor: (editor: TextEditor) => boolean,
private _changeWatchedFileFilter: (filePath: string) => boolean,
private _reportBusyWhile: ReportBusyWhile,
private _languageServerName: string
private _languageServerName: string,
private _determineProjectPath: (textEditor: TextEditor) => string | null
) {
this.updateNormalizedProjectPaths()
}
Expand Down Expand Up @@ -121,7 +124,7 @@ export class ServerManager {
textEditor: TextEditor,
{ shouldStart }: { shouldStart?: boolean } = { shouldStart: false }
): Promise<ActiveServer | null> {
const finalProjectPath = this.determineProjectPath(textEditor)
const finalProjectPath = this._determineProjectPath(textEditor)
if (finalProjectPath == null) {
// Files not yet saved have no path
return null
Expand Down Expand Up @@ -245,14 +248,6 @@ export class ServerManager {
})
}

public determineProjectPath(textEditor: TextEditor): string | null {
const filePath = textEditor.getPath()
if (filePath == null) {
return null
}
return this._normalizedProjectPaths.find((d) => filePath.startsWith(d)) || null
}

public updateNormalizedProjectPaths(): void {
this._normalizedProjectPaths = atom.project.getPaths().map(normalizePath)
}
Expand Down Expand Up @@ -350,3 +345,13 @@ export function normalizedProjectPathToWorkspaceFolder(normalizedProjectPath: st
export function normalizePath(projectPath: string): string {
return !projectPath.endsWith(path.sep) ? path.join(projectPath, path.sep) : projectPath
}

/** Considers a path for inclusion in `additionalPaths`. */
export function considerAdditionalPath(
server: ActiveServer & { additionalPaths: Set<string> },
additionalPath: string
): void {
if (!additionalPath.startsWith(server.projectPath)) {
server.additionalPaths.add(additionalPath)
}
}
100 changes: 89 additions & 11 deletions test/auto-languageclient.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { TextEditor } from "atom"
import AutoLanguageClient from "../lib/auto-languageclient"
import { projectPathToWorkspaceFolder, ServerManager } from "../lib/server-manager"
import {
projectPathToWorkspaceFolder,
ServerManager,
ActiveServer,
considerAdditionalPath,
normalizePath,
} from "../lib/server-manager"
import { FakeAutoLanguageClient } from "./helpers"
import { dirname } from "path"
import { dirname, join } from "path"

function mockEditor(uri: string, scopeName: string): any {
return {
Expand All @@ -12,20 +19,91 @@ function mockEditor(uri: string, scopeName: string): any {
}
}

function setupClient() {
atom.workspace.getTextEditors().forEach((editor) => editor.destroy())
atom.project.getPaths().forEach((project) => atom.project.removePath(project))
const client = new FakeAutoLanguageClient()
client.activate()
return client
}

function setupServerManager(client = setupClient()) {
/* eslint-disable-next-line dot-notation */
const serverManager = client["_serverManager"]
return serverManager
}

describe("AutoLanguageClient", () => {
describe("determineProjectPath", () => {
it("returns null when a single file is open", async () => {
const client = setupClient()
const textEditor = (await atom.workspace.open(__filename)) as TextEditor
/* eslint-disable-next-line dot-notation */
const projectPath = client["determineProjectPath"](textEditor)
expect(projectPath).toBeNull()
})
it("returns the project path when a file of that project is open", async () => {
// macos has issues with handling too much resources
Copy link
Member

Choose a reason for hiding this comment

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

is this just an issue on GitHub actions or would this crash Atom if run on a Mac?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because the number of tests is high, MacOS can't run all of them. It works fine if each of the tests runs in a single process. There is nothing OS-specific in the code, so this is a limitation of the operating system.

if (process.platform === "darwin") {
return
}
const client = setupClient()
const serverManager = setupServerManager(client)

const projectPath = __dirname

// gives the open workspace folder
atom.project.addPath(projectPath)
await serverManager.startServer(projectPath)

const textEditor = (await atom.workspace.open(__filename)) as TextEditor
/* eslint-disable-next-line dot-notation */
expect(client["determineProjectPath"](textEditor)).toBe(normalizePath(projectPath))
})
it("returns the project path for an external file if it is in additional paths", async () => {
// macos has issues with handling too much resources
if (process.platform === "darwin") {
return
}
aminya marked this conversation as resolved.
Show resolved Hide resolved

// "returns the project path when an external file is open and it is not in additional paths"

const client = setupClient()
const serverManager = setupServerManager(client)

const projectPath = __dirname
const externalDir = join(dirname(projectPath), "lib")
const externalFile = join(externalDir, "main.js")

// gives the open workspace folder
atom.project.addPath(projectPath)
await serverManager.startServer(projectPath)

let textEditor = (await atom.workspace.open(externalFile)) as TextEditor
/* eslint-disable-next-line dot-notation */
expect(client["determineProjectPath"](textEditor)).toBeNull()
textEditor.destroy()

// "returns the project path when an external file is open and it is in additional paths"

// get server
const server = serverManager.getActiveServers()[0]
expect(typeof server.additionalPaths).toBe("object") // Set()
// add additional path
considerAdditionalPath(server as ActiveServer & { additionalPaths: Set<string> }, externalDir)
expect(server.additionalPaths?.has(externalDir)).toBeTrue()

textEditor = (await atom.workspace.open(externalFile)) as TextEditor
/* eslint-disable-next-line dot-notation */
expect(client["determineProjectPath"](textEditor)).toBe(normalizePath(projectPath))
textEditor.destroy()
})
})
describe("ServerManager", () => {
describe("WorkspaceFolders", () => {
let client: FakeAutoLanguageClient
let serverManager: ServerManager

beforeEach(() => {
atom.workspace.getTextEditors().forEach((editor) => editor.destroy())
atom.project.getPaths().forEach((project) => atom.project.removePath(project))
client = new FakeAutoLanguageClient()
client.activate()

/* eslint-disable-next-line dot-notation */
serverManager = client["_serverManager"]
serverManager = setupServerManager()
})

afterEach(() => {
Expand Down