kopipasta 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kopipasta might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kopipasta
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: A CLI tool to generate prompts with project structure and file contents
5
5
  Home-page: https://github.com/mkorpela/kopipasta
6
6
  Author: Mikko Korpela
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env python3
2
2
  import os
3
3
  import argparse
4
+ import ast
4
5
  import re
6
+ from textwrap import dedent
5
7
  import pyperclip
6
8
  import fnmatch
7
9
 
@@ -88,6 +90,7 @@ def get_language_for_file(file_path):
88
90
  '.ts': 'typescript',
89
91
  '.tsx': 'tsx',
90
92
  '.html': 'html',
93
+ '.htm': 'html',
91
94
  '.css': 'css',
92
95
  '.json': 'json',
93
96
  '.md': 'markdown',
@@ -96,10 +99,251 @@ def get_language_for_file(file_path):
96
99
  '.yml': 'yaml',
97
100
  '.yaml': 'yaml',
98
101
  '.go': 'go',
99
- '.toml': 'toml'
102
+ '.toml': 'toml',
103
+ '.c': 'c',
104
+ '.cpp': 'cpp',
105
+ '.cc': 'cpp',
106
+ '.h': 'cpp',
107
+ '.hpp': 'cpp',
100
108
  }
101
109
  return language_map.get(extension, '')
102
110
 
111
+ def split_python_file(file_content):
112
+ """
113
+ Splits Python code into logical chunks using the AST module.
114
+ Returns a list of tuples: (chunk_code, start_line, end_line)
115
+ """
116
+ tree = ast.parse(file_content)
117
+ chunks = []
118
+ prev_end = 0
119
+ lines = file_content.splitlines(keepends=True)
120
+
121
+ def get_code(start, end):
122
+ return ''.join(lines[start:end])
123
+
124
+ for node in ast.iter_child_nodes(tree):
125
+ if hasattr(node, 'lineno'):
126
+ start_line = node.lineno - 1 # Convert to 0-indexed
127
+ end_line = getattr(node, 'end_lineno', None)
128
+ if end_line is None:
129
+ end_line = node.lineno
130
+ # Add code before the node
131
+ if prev_end < start_line:
132
+ chunks.append((get_code(prev_end, start_line), prev_end, start_line))
133
+ # Add the node code
134
+ chunks.append((get_code(start_line, end_line), start_line, end_line))
135
+ prev_end = end_line
136
+ # Add any remaining code at the end
137
+ if prev_end < len(lines):
138
+ chunks.append((get_code(prev_end, len(lines)), prev_end, len(lines)))
139
+ return chunks
140
+
141
+ def split_javascript_file(file_content):
142
+ """
143
+ Splits JavaScript code into logical chunks using regular expressions.
144
+ Returns a list of tuples: (chunk_code, start_line, end_line)
145
+ """
146
+ lines = file_content.splitlines(keepends=True)
147
+ chunks = []
148
+ pattern = re.compile(
149
+ r'^\s*(export\s+)?(async\s+)?(function\s+\w+|class\s+\w+|\w+\s*=\s*\(.*?\)\s*=>)',
150
+ re.MULTILINE
151
+ )
152
+ matches = list(pattern.finditer(file_content))
153
+
154
+ if not matches:
155
+ return [(file_content, 0, len(lines))]
156
+
157
+ prev_end_line = 0
158
+ for match in matches:
159
+ start_index = match.start()
160
+ start_line = file_content.count('\n', 0, start_index)
161
+ if prev_end_line < start_line:
162
+ code = ''.join(lines[prev_end_line:start_line])
163
+ chunks.append((code, prev_end_line, start_line))
164
+
165
+ function_code_lines = []
166
+ brace_count = 0
167
+ in_block = False
168
+ for i in range(start_line, len(lines)):
169
+ line = lines[i]
170
+ function_code_lines.append(line)
171
+ brace_count += line.count('{') - line.count('}')
172
+ if '{' in line:
173
+ in_block = True
174
+ if in_block and brace_count == 0:
175
+ end_line = i + 1
176
+ code = ''.join(function_code_lines)
177
+ chunks.append((code, start_line, end_line))
178
+ prev_end_line = end_line
179
+ break
180
+ else:
181
+ end_line = len(lines)
182
+ code = ''.join(function_code_lines)
183
+ chunks.append((code, start_line, end_line))
184
+ prev_end_line = end_line
185
+
186
+ if prev_end_line < len(lines):
187
+ code = ''.join(lines[prev_end_line:])
188
+ chunks.append((code, prev_end_line, len(lines)))
189
+
190
+ return chunks
191
+
192
+ def split_html_file(file_content):
193
+ """
194
+ Splits HTML code into logical chunks based on top-level elements using regular expressions.
195
+ Returns a list of tuples: (chunk_code, start_line, end_line)
196
+ """
197
+ pattern = re.compile(r'<(?P<tag>\w+)(\s|>).*?</(?P=tag)>', re.DOTALL)
198
+ lines = file_content.splitlines(keepends=True)
199
+ chunks = []
200
+ matches = list(pattern.finditer(file_content))
201
+
202
+ if not matches:
203
+ return [(file_content, 0, len(lines))]
204
+
205
+ prev_end = 0
206
+ for match in matches:
207
+ start_index = match.start()
208
+ end_index = match.end()
209
+ start_line = file_content.count('\n', 0, start_index)
210
+ end_line = file_content.count('\n', 0, end_index)
211
+
212
+ if prev_end < start_line:
213
+ code = ''.join(lines[prev_end:start_line])
214
+ chunks.append((code, prev_end, start_line))
215
+
216
+ code = ''.join(lines[start_line:end_line])
217
+ chunks.append((code, start_line, end_line))
218
+ prev_end = end_line
219
+
220
+ if prev_end < len(lines):
221
+ code = ''.join(lines[prev_end:])
222
+ chunks.append((code, prev_end, len(lines)))
223
+
224
+ return chunks
225
+
226
+ def split_c_file(file_content):
227
+ """
228
+ Splits C/C++ code into logical chunks using regular expressions.
229
+ Returns a list of tuples: (chunk_code, start_line, end_line)
230
+ """
231
+ pattern = re.compile(r'^\s*(?:[\w\*\s]+)\s+(\w+)\s*\([^)]*\)\s*\{', re.MULTILINE)
232
+ lines = file_content.splitlines(keepends=True)
233
+ chunks = []
234
+ matches = list(pattern.finditer(file_content))
235
+
236
+ if not matches:
237
+ return [(file_content, 0, len(lines))]
238
+
239
+ prev_end_line = 0
240
+ for match in matches:
241
+ start_index = match.start()
242
+ start_line = file_content.count('\n', 0, start_index)
243
+ if prev_end_line < start_line:
244
+ code = ''.join(lines[prev_end_line:start_line])
245
+ chunks.append((code, prev_end_line, start_line))
246
+
247
+ function_code_lines = []
248
+ brace_count = 0
249
+ in_function = False
250
+ for i in range(start_line, len(lines)):
251
+ line = lines[i]
252
+ function_code_lines.append(line)
253
+ brace_count += line.count('{') - line.count('}')
254
+ if '{' in line:
255
+ in_function = True
256
+ if in_function and brace_count == 0:
257
+ end_line = i + 1
258
+ code = ''.join(function_code_lines)
259
+ chunks.append((code, start_line, end_line))
260
+ prev_end_line = end_line
261
+ break
262
+ else:
263
+ end_line = len(lines)
264
+ code = ''.join(function_code_lines)
265
+ chunks.append((code, start_line, end_line))
266
+ prev_end_line = end_line
267
+
268
+ if prev_end_line < len(lines):
269
+ code = ''.join(lines[prev_end_line:])
270
+ chunks.append((code, prev_end_line, len(lines)))
271
+
272
+ return chunks
273
+
274
+ def split_generic_file(file_content):
275
+ """
276
+ Splits generic text files into chunks based on double newlines.
277
+ Returns a list of tuples: (chunk_code, start_line, end_line)
278
+ """
279
+ lines = file_content.splitlines(keepends=True)
280
+ chunks = []
281
+ start = 0
282
+ for i, line in enumerate(lines):
283
+ if line.strip() == '':
284
+ if start < i:
285
+ chunk_code = ''.join(lines[start:i])
286
+ chunks.append((chunk_code, start, i))
287
+ start = i + 1
288
+ if start < len(lines):
289
+ chunk_code = ''.join(lines[start:])
290
+ chunks.append((chunk_code, start, len(lines)))
291
+ return chunks
292
+
293
+ def select_file_patches(file_path):
294
+ file_content = read_file_contents(file_path)
295
+ language = get_language_for_file(file_path)
296
+ chunks = []
297
+ total_char_count = 0
298
+
299
+ if language == 'python':
300
+ code_chunks = split_python_file(file_content)
301
+ elif language == 'javascript':
302
+ code_chunks = split_javascript_file(file_content)
303
+ elif language == 'html':
304
+ code_chunks = split_html_file(file_content)
305
+ elif language in ['c', 'cpp']:
306
+ code_chunks = split_c_file(file_content)
307
+ else:
308
+ code_chunks = split_generic_file(file_content)
309
+
310
+ print(f"\nSelecting patches for {file_path}")
311
+ for index, (chunk_code, start_line, end_line) in enumerate(code_chunks):
312
+ print(f"\nChunk {index + 1} (Lines {start_line + 1}-{end_line}):")
313
+ print(f"```{language}\n{chunk_code}\n```")
314
+ while True:
315
+ choice = input("(y)es include / (n)o skip / (q)uit rest of file? ").lower()
316
+ if choice == 'y':
317
+ chunks.append(chunk_code)
318
+ total_char_count += len(chunk_code)
319
+ break
320
+ elif choice == 'n':
321
+ placeholder = get_placeholder_comment(language)
322
+ chunks.append(placeholder)
323
+ total_char_count += len(placeholder)
324
+ break
325
+ elif choice == 'q':
326
+ print("Skipping the rest of the file.")
327
+ return chunks, total_char_count
328
+ else:
329
+ print("Invalid choice. Please enter 'y', 'n', or 'q'.")
330
+
331
+ return chunks, total_char_count
332
+
333
+ def get_placeholder_comment(language):
334
+ comments = {
335
+ 'python': '# Skipped content\n',
336
+ 'javascript': '// Skipped content\n',
337
+ 'typescript': '// Skipped content\n',
338
+ 'java': '// Skipped content\n',
339
+ 'c': '// Skipped content\n',
340
+ 'cpp': '// Skipped content\n',
341
+ 'html': '<!-- Skipped content -->\n',
342
+ 'css': '/* Skipped content */\n',
343
+ 'default': '# Skipped content\n'
344
+ }
345
+ return comments.get(language, comments['default'])
346
+
103
347
  def get_file_snippet(file_path, max_lines=50, max_bytes=4096):
104
348
  snippet = ""
105
349
  byte_count = 0
@@ -166,7 +410,7 @@ def select_files_in_directory(directory, ignore_patterns, current_char_count=0):
166
410
  while True:
167
411
  if current_char_count > 0:
168
412
  print_char_count(current_char_count)
169
- file_choice = input(f"{file} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens) (y/n/q)? ").lower()
413
+ file_choice = input(f"{file} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens) (y/n/p/q)? ").lower()
170
414
  if file_choice == 'y':
171
415
  if is_large_file(file_path):
172
416
  while True:
@@ -186,11 +430,17 @@ def select_files_in_directory(directory, ignore_patterns, current_char_count=0):
186
430
  break
187
431
  elif file_choice == 'n':
188
432
  break
433
+ elif file_choice == 'p':
434
+ chunks, char_count = select_file_patches(file_path)
435
+ if chunks:
436
+ selected_files.append((file_path, False, chunks))
437
+ current_char_count += char_count
438
+ break
189
439
  elif file_choice == 'q':
190
440
  print(f"Quitting selection for {directory}")
191
441
  return selected_files, current_char_count
192
442
  else:
193
- print("Invalid choice. Please enter 'y', 'n', or 'q'.")
443
+ print("Invalid choice. Please enter 'y', 'n', 'p', or 'q'.")
194
444
  print(f"Added {len(selected_files)} files from {directory}")
195
445
  return selected_files, current_char_count
196
446
  elif choice == 'q':
@@ -277,10 +527,22 @@ def generate_prompt(files_to_include, ignore_patterns, web_contents, env_vars):
277
527
  prompt += get_project_structure(ignore_patterns)
278
528
  prompt += "\n```\n\n"
279
529
  prompt += "## File Contents\n\n"
280
- for file, use_snippet in files_to_include:
530
+ for file_tuple in files_to_include:
531
+ if len(file_tuple) == 3:
532
+ file, use_snippet, chunks = file_tuple
533
+ else:
534
+ file, use_snippet = file_tuple
535
+ chunks = None
536
+
281
537
  relative_path = get_relative_path(file)
282
538
  language = get_language_for_file(file)
283
- if use_snippet:
539
+
540
+ if chunks is not None:
541
+ prompt += f"### {relative_path} (selected patches)\n\n```{language}\n"
542
+ for chunk in chunks:
543
+ prompt += f"{chunk}\n"
544
+ prompt += "```\n\n"
545
+ elif use_snippet:
284
546
  file_content = get_file_snippet(file)
285
547
  prompt += f"### {relative_path} (snippet)\n\n```{language}\n{file_content}\n```\n\n"
286
548
  else:
@@ -332,13 +594,30 @@ def main():
332
594
  print(f"Added web content from: {input_path}")
333
595
  elif os.path.isfile(input_path):
334
596
  if not is_ignored(input_path, ignore_patterns) and not is_binary(input_path):
335
- use_snippet = is_large_file(input_path)
336
- files_to_include.append((input_path, use_snippet))
337
- if use_snippet:
338
- current_char_count += len(get_file_snippet(input_path))
339
- else:
340
- current_char_count += os.path.getsize(input_path)
341
- print(f"Added file: {input_path}{' (snippet)' if use_snippet else ''}")
597
+ while True:
598
+ file_choice = input(f"{input_path} (y)es include / (n)o skip / (p)atches / (q)uit? ").lower()
599
+ if file_choice == 'y':
600
+ use_snippet = is_large_file(input_path)
601
+ files_to_include.append((input_path, use_snippet))
602
+ if use_snippet:
603
+ current_char_count += len(get_file_snippet(input_path))
604
+ else:
605
+ current_char_count += os.path.getsize(input_path)
606
+ print(f"Added file: {input_path}{' (snippet)' if use_snippet else ''}")
607
+ break
608
+ elif file_choice == 'n':
609
+ break
610
+ elif file_choice == 'p':
611
+ chunks, char_count = select_file_patches(input_path)
612
+ if chunks:
613
+ files_to_include.append((input_path, False, chunks))
614
+ current_char_count += char_count
615
+ break
616
+ elif file_choice == 'q':
617
+ print("Quitting.")
618
+ return
619
+ else:
620
+ print("Invalid choice. Please enter 'y', 'n', 'p', or 'q'.")
342
621
  else:
343
622
  print(f"Ignored file: {input_path}")
344
623
  elif os.path.isdir(input_path):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kopipasta
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: A CLI tool to generate prompts with project structure and file contents
5
5
  Home-page: https://github.com/mkorpela/kopipasta
6
6
  Author: Mikko Korpela
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="kopipasta",
8
- version="0.4.0",
8
+ version="0.5.0",
9
9
  author="Mikko Korpela",
10
10
  author_email="mikko.korpela@gmail.com",
11
11
  description="A CLI tool to generate prompts with project structure and file contents",
File without changes
File without changes
File without changes
File without changes
File without changes