Allow the user to interactively enable additional languages when creating a project via CLI

Resolves #768
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2025-11-20 07:04:39 +01:00
1 parent 4385203956
commit 860e6b066e
5 files changed
+61 -19

No files matched your search

+1
View File
@@ -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
View File
@@ -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
+35 -5
View File
@@ -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,6 +138,7 @@ 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:
raise ValueError(
@@ -143,18 +151,40 @@ class ProjectConfig(ToolInclusionDefinition, ToStringMixin):
f" project_name: {project_name}\n"
f" language: python # or typescript, java, csharp, rust, go, ruby, cpp, php, swift, elixir, terraform, bash\n"
)
# 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)
+12
View 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.")
+7 -7
View File
@@ -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