mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 02:14:49 +00:00
Merge pull request #776 from oraios/pick-languages-cli
Allow the user to interactively enable additional languages
This commit is contained in:
5 files changed
+68
-26
No files matched your search
@@ -253,6 +253,7 @@ ignore = [
|
||||
"C400", # wants to unnecessarily force use of list comprehension
|
||||
"UP037", # can incorrectly (!) convert quoted type to unquoted type, causing an error
|
||||
"UP045", # imposes T | None instead of Optional[T]
|
||||
"UP031", # forbids % operator to format strings
|
||||
]
|
||||
unfixable = ["F841", "F601", "F602", "B018"]
|
||||
extend-fixable = ["F401", "B905", "W291"]
|
||||
|
||||
+6
-7
@@ -441,8 +441,11 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
raise ValueError(f"Unknown language '{lang}'. Supported: {all_langs}")
|
||||
|
||||
generated_conf = ProjectConfig.autogenerate(
|
||||
project_root=project_path, project_name=name, languages=languages if languages else None
|
||||
project_root=project_path, project_name=name, languages=languages if languages else None, interactive=True
|
||||
)
|
||||
yml_path = ProjectConfig.path_to_project_yml(project_path)
|
||||
languages_str = ", ".join([lang.value for lang in generated_conf.languages]) if generated_conf.languages else "N/A"
|
||||
click.echo(f"Generated project with languages {{{languages_str}}} at {yml_path}.")
|
||||
return generated_conf
|
||||
|
||||
@staticmethod
|
||||
@@ -462,10 +465,7 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
@click.option("--timeout", type=float, default=10, help="Timeout for indexing a single file (only used if --index is set).")
|
||||
def create(project_path: str, name: str | None, language: tuple[str, ...], index: bool, log_level: str, timeout: float) -> None:
|
||||
try:
|
||||
generated_conf = ProjectCommands._create_project(project_path, name, language)
|
||||
yml_path = os.path.join(project_path, ProjectConfig.rel_path_to_project_yml())
|
||||
click.echo(f"Generated project.yml with languages {generated_conf.languages} at {yml_path}.")
|
||||
|
||||
ProjectCommands._create_project(project_path, name, language)
|
||||
if index:
|
||||
click.echo("Indexing project...")
|
||||
ProjectCommands._index_project(project_path, log_level, timeout=timeout)
|
||||
@@ -497,8 +497,7 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
if not os.path.exists(yml_path):
|
||||
click.echo(f"Project configuration not found at {yml_path}. Auto-creating...")
|
||||
try:
|
||||
generated_conf = ProjectCommands._create_project(project, name, language)
|
||||
click.echo(f"Generated project.yml with languages {generated_conf.languages} at {yml_path}.")
|
||||
ProjectCommands._create_project(project, name, language)
|
||||
except FileExistsError:
|
||||
# Race condition - file was created between check and creation
|
||||
pass
|
||||
|
||||
@@ -34,6 +34,7 @@ from solidlsp.ls_config import Language
|
||||
|
||||
from ..analytics import RegisteredTokenCountEstimator
|
||||
from ..util.class_decorators import singleton
|
||||
from ..util.cli_util import ask_yes_no
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..project import Project
|
||||
@@ -113,7 +114,12 @@ class ProjectConfig(ToolInclusionDefinition, ToStringMixin):
|
||||
|
||||
@classmethod
|
||||
def autogenerate(
|
||||
cls, project_root: str | Path, project_name: str | None = None, languages: list[Language] | None = None, save_to_disk: bool = True
|
||||
cls,
|
||||
project_root: str | Path,
|
||||
project_name: str | None = None,
|
||||
languages: list[Language] | None = None,
|
||||
save_to_disk: bool = True,
|
||||
interactive: bool = False,
|
||||
) -> Self:
|
||||
"""
|
||||
Autogenerate a project configuration for a given project root.
|
||||
@@ -123,6 +129,7 @@ class ProjectConfig(ToolInclusionDefinition, ToStringMixin):
|
||||
containing the project
|
||||
:param languages: the languages of the project; if None, they will be determined automatically
|
||||
:param save_to_disk: whether to save the project configuration to disk
|
||||
:param interactive: whether to run in interactive CLI mode, asking the user for input where appropriate
|
||||
:return: the project configuration
|
||||
"""
|
||||
project_root = Path(project_root).resolve()
|
||||
@@ -131,30 +138,53 @@ class ProjectConfig(ToolInclusionDefinition, ToStringMixin):
|
||||
with LogTime("Project configuration auto-generation", logger=log):
|
||||
project_name = project_name or project_root.name
|
||||
if languages is None:
|
||||
# determine languages automatically
|
||||
language_composition = determine_programming_language_composition(str(project_root))
|
||||
if len(language_composition) == 0:
|
||||
language_values = ", ".join([lang.value for lang in Language])
|
||||
raise ValueError(
|
||||
f"No source files found in {project_root}\n\n"
|
||||
f"To use Serena with this project, you need to either:\n"
|
||||
f"1. Add source files in one of the supported languages (Python, JavaScript/TypeScript, Java, C#, Rust, Go, Ruby, C++, PHP, Swift, Elixir, Terraform, Bash)\n"
|
||||
f"2. Create a project configuration file manually at:\n"
|
||||
f" {os.path.join(project_root, cls.rel_path_to_project_yml())}\n\n"
|
||||
f"Example project.yml:\n"
|
||||
f" project_name: {project_name}\n"
|
||||
f" language: python # or typescript, java, csharp, rust, go, ruby, cpp, php, swift, elixir, terraform, bash\n"
|
||||
f"To use Serena with this project, you need to either\n"
|
||||
f" 1. specify a programming language by adding parameters --language <language>\n"
|
||||
f" when creating the project via the Serena CLI command OR\n"
|
||||
f" 2. add source files in one of the supported languages first.\n\n"
|
||||
f"Supported languages are: {language_values}\n"
|
||||
f"Read the documentation for more information."
|
||||
)
|
||||
# find the language with the highest percentage
|
||||
dominant_language = max(language_composition.keys(), key=lambda lang: language_composition[lang])
|
||||
languages_to_use = [dominant_language]
|
||||
# sort languages by number of files found
|
||||
languages_and_percentages = sorted(language_composition.items(), key=lambda item: item[1], reverse=True)
|
||||
# find the language with the highest percentage and enable it
|
||||
top_language_pair = languages_and_percentages[0]
|
||||
other_language_pairs = languages_and_percentages[1:]
|
||||
languages_to_use = [top_language_pair[0]]
|
||||
# if in interactive mode, ask the user which other languages to enable
|
||||
if len(other_language_pairs) > 0 and interactive:
|
||||
print(
|
||||
"Detected and enabled main language '%s' (%.2f%% of source files)." % (top_language_pair[0], top_language_pair[1])
|
||||
)
|
||||
print(f"Additionally detected {len(other_language_pairs)} other language(s).\n")
|
||||
print("Note: Enable only languages you need symbolic retrieval/editing capabilities for.")
|
||||
print(" Additional language servers use resources and some languages may require additional")
|
||||
print(" system-level installations/configuration (see Serena documentation).")
|
||||
print("\nWhich additional languages do you want to enable?")
|
||||
for lang, perc in other_language_pairs:
|
||||
enable = ask_yes_no("Enable %s (%.2f%% of source files)?" % (lang, perc), default=False)
|
||||
if enable:
|
||||
languages_to_use.append(lang)
|
||||
print()
|
||||
else:
|
||||
languages_to_use = [lang.value for lang in languages]
|
||||
config_with_comments = cls.load_commented_map(PROJECT_TEMPLATE_FILE)
|
||||
config_with_comments["project_name"] = project_name
|
||||
config_with_comments["languages"] = languages_to_use
|
||||
if save_to_disk:
|
||||
save_yaml(str(project_root / cls.rel_path_to_project_yml()), config_with_comments, preserve_comments=True)
|
||||
save_yaml(cls.path_to_project_yml(project_root), config_with_comments, preserve_comments=True)
|
||||
return cls._from_dict(config_with_comments)
|
||||
|
||||
@classmethod
|
||||
def path_to_project_yml(cls, project_root: str | Path) -> str:
|
||||
return os.path.join(project_root, cls.rel_path_to_project_yml())
|
||||
|
||||
@classmethod
|
||||
def rel_path_to_project_yml(cls) -> str:
|
||||
return os.path.join(SERENA_MANAGED_DIR_NAME, cls.SERENA_DEFAULT_PROJECT_FILE)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
def ask_yes_no(question: str, default: bool | None = None) -> bool:
|
||||
default_prompt = "Y/n" if default else "y/N"
|
||||
|
||||
while True:
|
||||
answer = input(f"{question} [{default_prompt}] ").strip().lower()
|
||||
if answer == "" and default is not None:
|
||||
return default
|
||||
if answer in ("y", "yes"):
|
||||
return True
|
||||
if answer in ("n", "no"):
|
||||
return False
|
||||
print("Please answer yes/y or no/n.")
|
||||
@@ -57,7 +57,7 @@ class TestProjectCreate:
|
||||
"""Test basic project creation with explicit language."""
|
||||
result = cli_runner.invoke(ProjectCommands.create, [temp_project_dir, "--language", "python"])
|
||||
assert result.exit_code == 0, f"Command failed: {result.output}"
|
||||
assert "Generated project.yml" in result.output
|
||||
assert "Generated project" in result.output
|
||||
assert "python" in result.output.lower()
|
||||
|
||||
# Verify project.yml was created
|
||||
@@ -68,7 +68,7 @@ class TestProjectCreate:
|
||||
"""Test project creation with auto-detected language."""
|
||||
result = cli_runner.invoke(ProjectCommands.create, [temp_project_dir_with_python_file])
|
||||
assert result.exit_code == 0, f"Command failed: {result.output}"
|
||||
assert "Generated project.yml" in result.output
|
||||
assert "Generated project" in result.output
|
||||
assert "python" in result.output.lower()
|
||||
|
||||
# Verify project.yml was created
|
||||
@@ -79,7 +79,7 @@ class TestProjectCreate:
|
||||
"""Test project creation with custom name and explicit language."""
|
||||
result = cli_runner.invoke(ProjectCommands.create, [temp_project_dir, "--name", "my-custom-project", "--language", "python"])
|
||||
assert result.exit_code == 0, f"Command failed: {result.output}"
|
||||
assert "Generated project.yml" in result.output
|
||||
assert "Generated project" in result.output
|
||||
|
||||
# Verify project.yml was created
|
||||
yml_path = os.path.join(temp_project_dir, ".serena", "project.yml")
|
||||
@@ -89,7 +89,7 @@ class TestProjectCreate:
|
||||
"""Test project creation with specified language."""
|
||||
result = cli_runner.invoke(ProjectCommands.create, [temp_project_dir, "--language", "python"])
|
||||
assert result.exit_code == 0, f"Command failed: {result.output}"
|
||||
assert "Generated project.yml" in result.output
|
||||
assert "Generated project" in result.output
|
||||
assert "python" in result.output.lower()
|
||||
|
||||
def test_create_with_multiple_languages(self, cli_runner, temp_project_dir):
|
||||
@@ -99,7 +99,7 @@ class TestProjectCreate:
|
||||
[temp_project_dir, "--language", "python", "--language", "typescript"],
|
||||
)
|
||||
assert result.exit_code == 0, f"Command failed: {result.output}"
|
||||
assert "Generated project.yml" in result.output
|
||||
assert "Generated project" in result.output
|
||||
|
||||
def test_create_with_invalid_language(self, cli_runner, temp_project_dir):
|
||||
"""Test project creation with invalid language raises error."""
|
||||
@@ -129,7 +129,7 @@ class TestProjectCreate:
|
||||
[temp_project_dir_with_python_file, "--language", "python", "--index", "--log-level", "ERROR", "--timeout", "5"],
|
||||
)
|
||||
assert result.exit_code == 0, f"Command failed: {result.output}"
|
||||
assert "Generated project.yml" in result.output
|
||||
assert "Generated project" in result.output
|
||||
assert "Indexing project" in result.output
|
||||
|
||||
# Verify project.yml was created
|
||||
@@ -144,7 +144,7 @@ class TestProjectCreate:
|
||||
"""Test that project creation without --index does NOT perform indexing."""
|
||||
result = cli_runner.invoke(ProjectCommands.create, [temp_project_dir, "--language", "python"])
|
||||
assert result.exit_code == 0
|
||||
assert "Generated project.yml" in result.output
|
||||
assert "Generated project" in result.output
|
||||
assert "Indexing" not in result.output
|
||||
|
||||
# Verify cache directory was NOT created
|
||||
|
||||
Reference in new issue
Block a user