Skip to content

Commit

Permalink
Merge pull request #160 from atom-community/configuring-project-path
Browse files Browse the repository at this point in the history
  • Loading branch information
aminya committed Jun 13, 2021
2 parents 813fbde + 61fec58 commit 69c6d3a
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 24 deletions.
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)
}
}
93 changes: 82 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,84 @@ 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 the project path for an internal or an external file in the project", async () => {
if (process.platform === "darwin") {
// there is nothing OS specific about the code. It just hits the limits that MacOS can handle in this test
pending("skipped on MacOS")
return
}
const client = setupClient()
const serverManager = setupServerManager(client)

// "returns null when a single file is open"

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

// "returns the project path when a file of that project is open"
const projectPath = __dirname

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

textEditor = (await atom.workspace.open(__filename)) as TextEditor
/* eslint-disable-next-line dot-notation */
expect(client["determineProjectPath"](textEditor)).toBe(normalizePath(projectPath))
textEditor.destroy()

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

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)

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

0 comments on commit 69c6d3a

Please sign in to comment.